DEV Community

Cover image for Beyond Login: Building a Production Authentication Lifecycle in FastAPI
HoungDev
HoungDev

Posted on

Beyond Login: Building a Production Authentication Lifecycle in FastAPI

Authentication is often presented as a short sequence:

  1. Accept a username and password.
  2. Return a JWT.
  3. Protect a few endpoints.

That is enough for a tutorial, but it is not an authentication lifecycle.

Real applications must also answer harder questions:

  • How is an email address verified without storing a reusable secret?
  • What happens to existing sessions after a password reset?
  • Can a user see and revoke a lost device?
  • How do we prevent a rotated refresh token from being replayed?
  • How should TOTP secrets and recovery codes be stored?
  • How can an OIDC identity be linked without trusting email matching?

I explored those questions while building
FastAPI Production API v1.2.0,
a backward-compatible authentication lifecycle release for an open-source
FastAPI backend foundation.

This article explains the design decisions behind it—not just the endpoints
that were added.

1. Model lifecycle tokens as scoped, single-use credentials

Email verification and password recovery look similar from the outside: send a
link, receive a token, and update an account. Treating them as interchangeable,
however, creates unnecessary risk.

The release uses account-action tokens with four important properties:

  • Random: the token is generated as an opaque secret rather than derived from user data.
  • Scoped: a verification token cannot be used as a password-reset token.
  • Expiring: every token has a short, configurable lifetime.
  • Single use: confirmation atomically marks the token as consumed.

Only a hash of the token is persisted. The original value exists only long
enough to be delivered to the user.

This gives email verification and password reset a shared security primitive
without making their policies identical.

The main endpoints are:

POST /auth/email-verification/request
POST /auth/email-verification/confirm
POST /auth/password-reset/request
POST /auth/password-reset/confirm
Enter fullscreen mode Exit fullscreen mode

Both request operations return uniform responses. A caller should not be able
to determine whether an email belongs to an account by comparing status codes
or response bodies.

2. Treat password reset as a session-security event

Changing a password is not only a database update. If a stolen refresh token
remains valid afterward, the attacker may keep creating new access tokens even
though the account owner completed recovery.

A successful reset therefore performs one transaction that:

  1. validates and consumes the scoped reset token;
  2. replaces the password hash;
  3. consumes other outstanding reset tokens; and
  4. revokes every refresh-token session for the account.

The endpoint does not issue a new authenticated session. The user signs in
again with the new password.

There is an important boundary here: existing stateless access tokens remain
valid until their short expiration time. Immediate access-token invalidation
would require a denylist, token-version check, or a move toward opaque tokens.
Documenting that boundary is part of the security design.

3. Use refresh-token families to represent device sessions

A table of individual refresh tokens does not naturally answer “Which devices
are signed in?” Token rotation creates a chain of records, while the user thinks
in terms of sessions.

v1.2.0 groups rotated refresh tokens into a family:

login
  -> create family A for "Work Laptop"
  -> issue refresh token A1

refresh A1
  -> revoke A1
  -> issue A2 inside family A
Enter fullscreen mode Exit fullscreen mode

This family becomes the device-session boundary. It supports:

GET    /auth/sessions
DELETE /auth/sessions/{session_id}
DELETE /auth/sessions
Enter fullscreen mode Exit fullscreen mode

Device labels are bounded and normalized before storage. Session responses
expose useful metadata without returning token hashes or raw credentials.

The family also improves replay handling. If a refresh token that was already
consumed by rotation appears again, the live family is revoked. The system
treats reuse as possible token theft rather than an ordinary validation error.

Logout revokes the full family, not only one token record. Password reset, MFA
changes, and external-identity changes can revoke all relevant families through
the same session abstraction.

4. Add TOTP without turning the seed into a password

TOTP verification requires access to the shared secret, so hashing the seed is
not sufficient. The implementation encrypts TOTP seeds at rest using a
dedicated application key.

Recovery codes have different requirements. They only need comparison, so the
database stores hashes and shows the original codes once when they are created.
Each recovery code is single use.

Enrollment is a two-step process:

POST /auth/mfa/totp/enroll
POST /auth/mfa/totp/confirm
Enter fullscreen mode Exit fullscreen mode

The first operation creates a pending encrypted secret. MFA is not enabled
until the user proves possession by submitting a valid code.

After enrollment, password login no longer returns access and refresh tokens
immediately. It returns a short-lived opaque challenge:

password accepted
  -> mfa_required
  -> challenge_token
  -> TOTP or unused recovery code
  -> local access and refresh tokens
Enter fullscreen mode Exit fullscreen mode

The service records the last accepted TOTP counter and rejects the same or an
older counter. This prevents a valid code from being replayed inside the
accepted clock window.

Access tokens include authentication-method and authentication-time claims.
Those claims provide a foundation for requiring recent MFA before sensitive
operations. Refreshing a session intentionally does not manufacture a recent
MFA event.

TOTP is useful, but it is not phishing resistant. WebAuthn/passkeys remain the
stronger direction for applications with that requirement.

5. Make OIDC linking an explicit security ceremony

OIDC login uses the Authorization Code flow with:

  • PKCE S256;
  • random state;
  • OIDC nonce;
  • exact configured redirect URIs;
  • browser-bound authorization transactions; and
  • strict issuer, signature, audience, authorized-party, and nonce validation.

The database stores hashes of state, nonce, and browser binding. The
transaction-specific PKCE verifier must later be sent to the provider, so it is
encrypted rather than hashed.

External accounts are identified by the immutable pair (issuer, subject).
Provider email is useful profile data, but it is not the identity key.

Most importantly, an existing local account is never silently linked because
an OIDC provider returned the same email address. Automatic email matching can
turn a provider mistake or account-reassignment edge case into account takeover.

Linking is therefore explicit and authenticated:

POST /auth/oidc/link/authorize
GET  /auth/oidc/callback
GET  /auth/oidc/identities
DELETE /auth/oidc/identities/{identity_id}
Enter fullscreen mode Exit fullscreen mode

Sensitive link and unlink operations require recent authentication and revoke
refresh sessions after the identity set changes. The application also prevents
a user from removing their last available sign-in method.

OIDC does not bypass local MFA. If a linked local account has MFA enabled, a
successful provider response produces the same second-factor challenge used by
password login.

What made this a release rather than a demo?

The implementation was shipped through five focused slices:

  1. Email identity and verification
  2. Password recovery and session revocation
  3. Refresh-token families and device sessions
  4. TOTP MFA and recovery codes
  5. OIDC login and explicit account linking

Every slice included tests, documentation, failure paths, and migration checks.
The final v1.2.0 release passed:

  • 144 automated tests;
  • 92.85% coverage against a 90% gate;
  • linting and formatting checks;
  • Alembic migration and rollback validation;
  • dependency auditing;
  • source distribution and wheel builds; and
  • an isolated wheel installation and application-version smoke test.

The release also publishes SHA-256 checksums alongside the Python artifacts.

Try it or review the design

Repository:
github.com/HoungDev/fastapi-production-api

Release:
FastAPI Production API v1.2.0

The project is intended as a security-focused reference foundation, not a claim
that one authentication policy fits every production system. Provider setup,
email delivery, secret management, token lifetimes, access-token invalidation,
and threat models still need application-specific decisions.

I would especially value feedback on the refresh-family model, MFA step-up
claims, and OIDC linking rules.

I also shared the release summary on
LinkedIn.

What authentication lifecycle failure mode has caused the most trouble in your
own systems?

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.