fastapi-passkeys
Passkeys / WebAuthn authentication for FastAPI — phishing-resistant passwordless login with cloned-authenticator detection, single-use challenges, and strict origin checks, without locking into a specific auth library or ORM.
If this project helps you, please ⭐ star the repo — it really helps others find it.
·
Table of contents
- Status
- Why
- Install
- Quickstart
- Endpoints
- Storage backends
- Security
- Documentation
- Roadmap
- Contributing
- Support the project
- License
Status
0.1.1 — Alpha. Full registration + authentication ceremonies, clone
detection, and SQLAlchemy/Redis adapters. The public API may change before
1.0. See CHANGELOG.md and the roadmap.
Why
Passwords are a liability and WebAuthn is easy to get subtly wrong — dropped
signature counters, replayable challenges, missing origin checks.
fastapi-passkeys owns the hard, security-sensitive part and stays out of the
way of your idea of a session:
- Secure by default — single-use, TTL-bound challenges; strict origin/RP
validation; user-verification policy; and monotonic
sign_countcloned- authenticator detection (the prior Django solution stores no counter at all). - Auth-agnostic — it verifies the passkey and hands you the user; you mint
whatever session or JWT you like via an
on_authenticatedhook. - Storage-abstracted — credentials live behind an async
CredentialRepositoryprotocol (in-memory, stateless, SQLAlchemy 2.0, and Redis adapters included), with a shipped contract test-suite for your own. - Async-native and fully typed (
mypy --strict,py.typed).
Install
pip install fastapi-passkeys
# optional extras:
pip install "fastapi-passkeys[sqlalchemy]" # SQLAlchemy 2.0 async credential repo
pip install "fastapi-passkeys[redis]" # Redis challenge store (atomic single-use)
Quickstart
from fastapi import FastAPI, Request
from fastapi_passkeys import (
AuthenticationResult,
Passkeys,
PasskeyConfig,
PasskeyUser,
)
from fastapi_passkeys.contrib import InMemoryCredentialRepository
async def get_user(request: Request) -> PasskeyUser:
# However your app identifies the in-progress user: a signup token, an
# existing session, an email from the request body, etc.
return PasskeyUser(id="user-123", name="ada@example.com", display_name="Ada Lovelace")
async def on_authenticated(request: Request, result: AuthenticationResult) -> dict:
# The passkey is verified — now mint *your* session or token.
return {"access_token": issue_token(result.user_id)}
passkeys = Passkeys(
config=PasskeyConfig(
rp_id="example.com",
rp_name="Example",
expected_origins=["https://example.com"],
),
credential_repository=InMemoryCredentialRepository(),
get_user=get_user,
on_authenticated=on_authenticated,
)
app = FastAPI()
app.include_router(passkeys.router, prefix="/auth/passkeys", tags=["passkeys"])
passkeys.install_exception_handlers(app)
Each ceremony is a begin → finish pair: begin returns the options your
frontend passes to navigator.credentials.create() / .get() plus an opaque
state handle; echo that state back to finish with the authenticator's
response. No server session is required between the calls. A full, runnable
browser demo lives in examples/app.py.
Need full control? Skip the router and drive passkeys.registration /
passkeys.authentication (the services) from your own endpoints.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/register/begin |
Start registration; returns creation options + state |
POST |
/register/finish |
Verify attestation and store the credential |
POST |
/authenticate/begin |
Start authentication (usernameless or with userId) |
POST |
/authenticate/finish |
Verify assertion; runs your on_authenticated hook |
GET |
/credentials |
List the current user's passkeys |
PATCH |
/credentials/{id} |
Rename a passkey |
DELETE |
/credentials/{id} |
Revoke a passkey |
Storage backends
| Backend | Import | Extra |
|---|---|---|
| In-memory credentials | fastapi_passkeys.contrib.InMemoryCredentialRepository |
— |
| In-memory challenges | fastapi_passkeys.contrib.InMemoryChallengeStore |
— |
| Stateless challenges | fastapi_passkeys.contrib.StatelessChallengeStore |
— |
| SQLAlchemy credentials | fastapi_passkeys.contrib.sqlalchemy.SqlAlchemyCredentialRepository |
[sqlalchemy] |
| Redis challenges | fastapi_passkeys.contrib.redis.RedisChallengeStore |
[redis] |
Implement the CredentialRepository / ChallengeStore protocols for any other
store (SQLModel, Tortoise, Beanie, …) and validate it with the shipped contract
suite in fastapi_passkeys.testing.
Security
- Challenges are CSPRNG-generated, bound to user + ceremony, TTL-enforced server-side, and single-use (in-memory and Redis stores).
- Clone detection stores and enforces the signature counter; a regression is rejected (and optionally auto-disables the credential) and audited.
- Origins & RP ID are strictly validated; multiple origins are supported.
- No secrets are logged — audit events carry identifiers and outcomes only.
See the security model
for the full picture, including the stateless-store tradeoff. Report
vulnerabilities privately via SECURITY.md.
Documentation
Full docs: https://javlondevv.github.io/fastapi-passkeys/
- Quickstart
- Concepts — a short WebAuthn primer
- Storage backends
- Security model
- Migrating from django-passkeys
Roadmap
- 0.1 — core ceremonies, clone detection, router + services, in-memory / stateless / SQLAlchemy / Redis adapters, contract suite, docs site.
- 0.2 — username-first resolution hooks, conditional-UI helpers, attestation verification options, rate-limiting guidance.
- 0.3 — SQLModel / Tortoise / Beanie adapters, credential metadata events, admin/management helpers.
- 1.0 — API freeze + semver guarantee.
Contributing
Contributions are very welcome! See CONTRIBUTING.md for the
dev setup and checks. Good entry points are the issues labelled
good first issue
and help wanted.
Please also read our Code of Conduct.
Support the project
The simplest way to help is a ⭐ star — it boosts visibility for everyone.
Embed a live star button on your own site or docs with GitHub Buttons:
<!-- Place once, before </body> -->
<a class="github-button"
href="https://github.com/javlondevv/fastapi-passkeys"
data-icon="octicon-star"
data-size="large"
data-show-count="true"
aria-label="Star javlondevv/fastapi-passkeys on GitHub">Star</a>
<script async defer src="https://buttons.github.io/buttons.js"></script>
License
MIT — see LICENSE.
Metadata
Release files for fastapi-passkeys 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_passkeys-0.1.2.tar.gz | 39.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_passkeys-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 80.3 kB
Release files / fastapi_passkeys-0.1.2.tar.gz
| Download URL | fastapi_passkeys-0.1.2.tar.gz |
|---|---|
| Size | 39.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6047a7690edbe47f11a98f3611a16132fed9b5ef4619002932de91c6bb00228
|
|
BLAKE2b-256 checksum How to use checksums |
f4349d97dcc02c09eab20ffeb1472b0844e0b8106e8460aa2d1dd349561eeb25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.
Transparency logRelease files / fastapi_passkeys-0.1.2-py3-none-any.whl
| Download URL | fastapi_passkeys-0.1.2-py3-none-any.whl |
|---|---|
| Size | 40.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
217303f73df0a91ffbd3916d90f6497aac2409db6deb811ae6ab39aebec69e7a
|
|
BLAKE2b-256 checksum How to use checksums |
d85b872a0e949bbbd2661f8670529b78cf64691c1ccd1b43520981bf9b3fa279
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.
Transparency log