How to Add Auth to a FastAPI App (OAuth2, JWT, Dependencies)
A practical guide to adding authentication to a FastAPI app: OAuth2PasswordBearer, JWT tokens, dependency injection, and fastapi-users — with code.
FastAPI gives you typed routes, automatic docs, and dependency injection, and then leaves authentication mostly up to you. There’s no User model in the box, no login view, no session store. Adding auth to a FastAPI app means assembling the pieces — OAuth2 password flow, JWT signing, a get_current_user dependency — and getting the security details right yourself.
This guide covers the standard FastAPI auth pattern with real code, where JWT helps and where it bites, and the trade-offs that matter when you’re shipping an API-backed SaaS solo.
The Standard FastAPI Pattern: OAuth2 + JWT
The canonical FastAPI approach is the OAuth2 password flow with JWT bearer tokens. It has three parts: a token endpoint that verifies credentials and signs a JWT, a security scheme that pulls the token off requests, and a dependency that decodes it into a user.
First, hash passwords properly. Use a vetted library — passlib with bcrypt is the common choice:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(p: str) -> str:
return pwd_context.hash(p)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
Next, sign tokens. Keep the secret in an environment variable and always set an expiry:
from datetime import datetime, timedelta, timezone
import jwt # PyJWT
import os
SECRET_KEY = os.environ["SECRET_KEY"]
ALGORITHM = "HS256"
def create_access_token(sub: str, minutes: int = 30) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
return jwt.encode(
{"sub": sub, "exp": expire}, SECRET_KEY, algorithm=ALGORITHM
)
The token endpoint uses FastAPI’s OAuth2PasswordRequestForm, which makes the interactive /docs “Authorize” button work:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
def login(form: OAuth2PasswordRequestForm = Depends()):
user = get_user(form.username)
if not user or not verify_password(form.password, user.password_hash):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid credentials")
token = create_access_token(sub=str(user.id))
return {"access_token": token, "token_type": "bearer"}
Protecting Routes with a Dependency
Here’s where FastAPI’s design pays off. Authentication is just a dependency, so any route can require a user by declaring it as a parameter:
def get_current_user(token: str = Depends(oauth2_scheme)):
creds_error = HTTPException(
status.HTTP_401_UNAUTHORIZED,
"Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise creds_error
except jwt.PyJWTError:
raise creds_error
user = get_user_by_id(user_id)
if user is None:
raise creds_error
return user
@app.get("/me")
def read_me(user = Depends(get_current_user)):
return {"id": user.id, "email": user.email}
oauth2_scheme reads the Authorization: Bearer <token> header, get_current_user decodes and validates it, and every protected endpoint just adds user = Depends(get_current_user). Clean and composable. You can layer on role checks by wrapping that dependency in another.
The JWT Trade-off You Have to Plan For
JWTs are stateless: the server verifies the signature and trusts the claims, no database lookup per request. That’s great for performance and for distributed systems. But it has a sharp edge — a JWT stays valid until it expires. There’s no built-in way to revoke one. If a user logs out, or you ban an account, or a token leaks, it keeps working until exp.
Two mitigations, both of which you build yourself:
- Short-lived access tokens + refresh tokens. Access tokens expire in minutes; a longer-lived refresh token (stored server-side, revocable) mints new ones. Logout invalidates the refresh token.
- A blocklist. Keep revoked token IDs in Redis and check them in
get_current_user. This reintroduces a per-request lookup, which partly defeats the point of JWT — but it’s sometimes the right call.
If you don’t need stateless tokens across services, server-side sessions (a session ID cookie backed by Redis) are simpler to revoke. For a single API with a web frontend, that’s often the better default despite JWT being the popular tutorial answer.
Option: fastapi-users (don’t build it all by hand)
If assembling registration, password reset, email verification, and OAuth from scratch sounds like a week you don’t have, fastapi-users packages those flows with pluggable database and auth backends. It’s the closest thing FastAPI has to a batteries-included auth library and worth using over hand-rolling the full account lifecycle. You still configure the database layer and email sending, but you skip writing the verification and reset logic yourself.
If you’re weighing token auth against passwordless flows, the magic link authentication guide covers when email-based login is the simpler path.
Where This Goes From “Login Works” to “Weeks of Work”
The /token and /me endpoints above are the easy part. A real SaaS API needs the rest: registration with email verification, password reset tokens that expire, rate limiting on the login route, refresh-token rotation, and — once you want revenue — Stripe. That means creating a Stripe customer per user, webhook endpoints for subscription events, plan gating in your dependencies, and keeping auth and billing state in sync forever.
Beag collapses that. Instead of building auth and Stripe billing into your FastAPI service, Beag provides server-validated authentication and Stripe subscriptions together, with no auth backend or webhook handlers for you to maintain. The user’s plan and status are available to your frontend after login, so feature gating is a plan check instead of a billing subsystem:
def require_pro(user = Depends(get_current_user)):
if user.plan not in ("pro", "enterprise"):
raise HTTPException(403, "Upgrade required")
return user
You keep FastAPI for your API and skip building auth and payments infrastructure from scratch.
What to Do
- Pure API for a mobile or third-party client? OAuth2 + JWT with short-lived access tokens and refresh tokens.
- API behind your own web app? Consider server-side sessions; they’re easier to revoke.
- Don’t want to build registration/reset/verification? Use fastapi-users.
- Need payments live this week? Don’t hand-roll auth-plus-Stripe — use an all-in-one so identity and billing stay in sync.
Set up auth before your endpoints multiply. Every route needs a guard and every record needs an owner — doing it first means the rest of your API assumes a known user.
Add auth and Stripe payments to your FastAPI app fast. Get started with Beag — no webhook endpoints, no billing subsystem.
For more guides, see how to add auth to a Next.js app or browse the Beag blog.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →