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.
·
- Status
- Why
- Install
- Quickstart
- Endpoints
- Storage backends
- Security
- Documentation
- Roadmap
- Contributing
- Support the project
- License
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.
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).
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)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.
| 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 |
| 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.
- 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.
Full docs: https://javlondevv.github.io/fastapi-passkeys/
- Quickstart
- Concepts — a short WebAuthn primer
- Storage backends
- Security model
- Migrating from django-passkeys
- 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.
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.
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>MIT — see LICENSE.