FastAPI's documentation makes authentication look effortless. A few decorators, a Depends call, and you have a working bearer token flow. That ease is also the problem. Most of the FastAPI auth code we review in production looks like the tutorial extended by a few features, and the gaps that result are predictable: missing audience validation, refresh tokens that never expire, dependency ordering bugs that let the wrong claim flow through. This post is the version of the tutorial we wish more teams had read first.
We are not going to rehash OAuth2Password and HTTPBearer. Instead, we will focus on the design decisions that determine whether your auth survives a serious adversary and a six-month code review cycle.
What token format should I use?
Use JWTs only if you genuinely need stateless validation. The reflexive choice of JWT for every API is one of the most common over-engineering moves in FastAPI codebases. If your auth flow already involves a hit to Redis or Postgres on every request, opaque tokens with a session lookup are simpler, easier to revoke, and avoid the entire family of JWT cryptographic missteps. Stripe and GitHub both ship API tokens as opaque strings, and neither company is short on auth expertise.
If you do use JWTs, pin the algorithm explicitly. The python-jose and pyjwt libraries both default to permissive algorithm acceptance unless you pass algorithms=["RS256"] or whatever you actually use. CVE-2022-29217 and the family of alg=none bypasses still appear in pen tests as recently as Q4 2025. Pin the algorithm, validate the audience, validate the issuer, and verify expiry. All four. Every time.
How should I structure dependencies?
The FastAPI dependency system is powerful and it is also where most auth bugs originate. The pattern we recommend is a layered chain: a low-level dependency that extracts and validates the token, a middle dependency that loads the user from the validated claims, and high-level dependencies that enforce specific permissions. Each layer has one responsibility. Each layer raises 401 or 403 with a clear error.
The anti-pattern we see most often is the god-dependency that does token validation, user loading, permission checking, and rate limiting in one function. It works until someone adds a route that needs a slightly different permission set and copy-pastes 80% of the dependency. Two months later you have four near-identical auth dependencies and a bug in three of them. Keep the layers separated, test each layer independently, and compose them at the route definition.
How do I handle refresh tokens correctly?
Refresh tokens are where production FastAPI auth goes wrong most often. The common mistake: refresh tokens that are themselves JWTs with the same expiry behavior as access tokens, no rotation, and no revocation list. That is functionally equivalent to a long-lived access token with extra ceremony.
The pattern that holds up is refresh-token rotation with reuse detection. Each refresh produces a new refresh token and invalidates the old one. If a previously-used refresh token is presented again, you treat that as a compromise signal and invalidate the entire token family for that user. This is the OAuth 2.1 recommendation and it requires server-side state. That state is one row per active session in your existing database, which is cheap. Store the token family ID, the current token hash, the user ID, and the rotation counter. Auth0 and Okta both ship this pattern by default in 2026, which tells you where the consensus has landed.
What about session cookies vs bearer tokens?
For browser-facing applications, session cookies with the HttpOnly, Secure, and SameSite=Lax flags remain the right default in 2026. Bearer tokens in localStorage are still being recommended in tutorials and they should not be. The XSS exposure is materially worse and the only argument for localStorage tokens is convenience for SPA developers, which is not a security argument.
For server-to-server and mobile flows, bearer tokens are appropriate, and you should treat them as credentials with proper handling: short access token TTLs of 15 to 60 minutes, refresh rotation as above, and a clear revocation path. FastAPI supports both patterns cleanly through the same dependency system; the choice is about the client, not the framework.
How do I avoid the most common production bugs?
The bugs we see repeatedly in FastAPI auth code reviews: missing audience claim validation, which lets a token issued for service A be replayed against service B; missing token type checks, which lets a refresh token be used where an access token was expected; trusting request.headers["X-Forwarded-User"] from a misconfigured proxy chain; and forgetting that Depends results are cached within a single request, so dependency-based rate limiting silently breaks if you put it at the wrong layer.
The audit pattern that catches most of these is a test suite that explicitly tries the malicious cases. Token for the wrong audience. Refresh token used as access token. Expired token. Token signed with HS256 when you expect RS256. Five or six tests, written once, run on every PR. The tests pay for themselves the first time they catch a regression, which has been every single time we have seen them deployed.
How Safeguard Helps
Safeguard's reachability analysis flags vulnerable versions of python-jose, pyjwt, and adjacent auth libraries based on the call paths actually exercised in your FastAPI codebase, not just the package version. Griffin AI surfaces emerging auth-related CVEs against your dependency tree within hours of disclosure, including the long tail of JWT library issues that pen testers still exploit in 2026. Policy gates in CI can require that authentication code paths have explicit test coverage before merge, and our SBOM pipeline tracks the full transitive surface of your auth stack so you can answer "what version of cryptography are we actually shipping" without running a debugger in production.