How to Add JWT Authentication to a FastAPI API
JWT authentication on a FastAPI API means two things: a login endpoint that signs and returns a token, and a dependency that verifies the bearer token on every protected route. Here's how to wire both using PyJWT.
How JWT auth fits into FastAPI
The pattern has two parts. First, POST /auth/login validates credentials and
returns a signed JWT. Second, a require_auth dependency reads the
Authorization: Bearer <token> header on protected routes, decodes and verifies
the token with PyJWT, and raises HTTPException(401) if anything is wrong.
Routes that don't need auth - /health, /auth/login - simply don't declare
the dependency.
Issue a signed token on login
import os
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import FastAPI, HTTPException
app = FastAPI()
JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "HS256"
@app.post("/auth/login")
def login(credentials: dict):
if credentials.get("username") != "alice" or credentials.get("password") != "s3cret":
raise HTTPException(status_code=401, detail="Invalid credentials")
payload = {
"sub": credentials["username"],
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
}
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return {"token": token}
sub (subject) stores the username in the token claims. exp sets the
expiry - PyJWT enforces it automatically on decode.
Verify the token in a dependency
from fastapi import Depends, Header
def require_auth(authorization: str = Header(None)) -> dict:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing bearer token")
token = authorization.removeprefix("Bearer ").strip()
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/api/users")
def list_users(claims: dict = Depends(require_auth)):
return {"users": [...]}
@app.get("/api/users/me")
def get_me(claims: dict = Depends(require_auth)):
return {"username": claims["sub"]}
Depends(require_auth) injects the decoded claims into the route handler.
You get the username from claims["sub"] without another DB lookup.
Test the full flow
# Start the server
uvicorn main:app --host 0.0.0.0 --port 8000
# Login and capture the token
TOKEN=$(curl -sf -X POST http://localhost:8000/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"s3cret"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
# Authenticated call
curl http://localhost:8000/api/users -H "Authorization: Bearer $TOKEN"
# No token - should return 401
curl -i http://localhost:8000/api/users
What to know before production
- Short expiry + refresh tokens - 1 hour is fine for a prototype; real apps issue short-lived access tokens and a separate refresh token so users aren't kicked out.
- RS256 for multi-service - if other services need to verify your tokens, switch to asymmetric signing (RS256). The private key signs; public keys verify. With HS256 every verifier needs the shared secret.
kidheader for key rotation - add a key ID to the header so you can rotate secrets without invalidating all live tokens at once.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Issuing a signed JWT with PyJWT on a login endpoint
- Writing a FastAPI dependency that verifies the Authorization bearer token
- Reading token claims (sub) in a protected route handler
FAQ
How do I protect FastAPI routes with JWT?
Write a dependency function that reads the Authorization header, strips 'Bearer ', and calls jwt.decode. Wire it to protected routes with Depends(require_auth). Public routes like /health simply don't declare the dependency.
How do I sign a JWT with PyJWT?
Call jwt.encode({'sub': username, 'exp': datetime.now(timezone.utc) + timedelta(hours=1)}, secret, algorithm='HS256'). PyJWT returns the token string you can return in the response body.
What errors does jwt.decode raise for bad tokens?
PyJWT raises ExpiredSignatureError for tokens past their exp claim, and InvalidTokenError (the base class) for wrong signatures, missing fields, or malformed tokens. Catch both and return 401.
Which is better, OAuth or JWT?
They solve different problems. OAuth is an authorization framework (how a user grants an app access); JWT is a token format often used to carry that authorization. For a simple API login, JWT alone is enough; OAuth often issues a JWT.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →