Search Topics
Search across all FastAPI topics
OAuth2 & JWT
Core ConceptFastAPI has built-in support for OAuth2 with Password flow. Combined with JWT tokens, you get a complete authentication system with automatic Swagger UI integration.
A user logs in and gets a JWT token. It works perfectly for weeks. Then you realize the token was set to expire in 30 days instead of 30 minutes. A compromised token gives an attacker a month of access to your API.
# JWT Payload (decoded):
{
"sub": "alice@example.com",
"exp": 1737590400, // ← 30 DAYS from now!
"iat": 1735000000
}
# Alice's laptop gets stolen on day 2.
# The thief has 28 more days of full API access.
# Alice changes her password. The token STILL works.
# JWTs are stateless — the server can't revoke them.Question
So how do you protect your API when tokens can't be revoked? The answer is: keep them short-lived. A 30-minute token means a stolen credential is only dangerous for 30 minutes, not 30 days.
Let's build an OAuth2 + JWT auth system from scratch and make sure you don't fall into this trap.
How JWT Auth Actually Works
Before we write a single line of code, here's the flow you're building. Every JWT auth system follows this exact pattern.
User POSTs credentials
username + password to /token
Server verifies
Checks password hash
Server creates JWT
Signs with SECRET_KEY
Client stores token
Sends it in Authorization header
Server decodes JWT
Validates signature + expiry
The Big Picture
What you just learned
JWTs are stateless — the server doesn't store sessions
The token carries its own data (user ID, expiry) inside it
The signature proves the token wasn't tampered with
Short expiry times are your main defense against stolen tokens
Setting Up OAuth2
First, you need two things: an OAuth2 scheme that tells FastAPI where to find the token, and a function that creates tokens with a proper expiry. Notice we set ACCESS_TOKEN_EXPIRE_MINUTES = 30 — not 30 days.
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from datetime import datetime, timedelta, timezone
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "your-secret-key" # In production, use a real secret!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # ← 30 MINUTES, not days
def create_access_token(data: dict):
to_encode = data.copy()
# This is the critical part — the expiry
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)Watch out
See that SECRET_KEY = "your-secret-key"? In production, that needs to be a long, random string stored in an environment variable. If someone discovers your secret key, they can forge valid tokens for any user.
The Token Endpoint
This is where users exchange their username and password for a JWT. FastAPI's OAuth2PasswordRequestForm gives you a form that works automatically with the Swagger UI "Authorize" button.
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Step 1: Check if the user exists and password is correct
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, # OAuth2 spec requires this
)
# Step 2: Create a token with the user's identity
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}Protecting Your Endpoints
Now for the magic part. You create a dependency that decodes the JWT from every request. If the token is invalid, expired, or missing — the user gets a 401 before your endpoint code even runs.
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# Decode the JWT — this checks the signature AND expiry
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
# Token is invalid, expired, or tampered with
raise credentials_exception
user = get_user(username)
if user is None:
raise credentials_exception
return user
# Now just add the dependency — that's it!
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user # Only runs if token is validAuthentication Flow
What you just learned
OAuth2PasswordBearer extracts the token from the Authorization header
jwt.decode() verifies the signature AND checks if the token is expired
The dependency pattern means you write the auth logic once and reuse it everywhere
Swagger UI gets a working Authorize button automatically from OAuth2PasswordBearer
Go Deeper: What About Token Revocation?
Remember our opening scenario? Alice's laptop gets stolen, but the JWT still works. Short expiry helps, but what if you need to revoke a token immediately?
JWTs are stateless by design — the server doesn't track them. So you have a few options:
Short Expiry
30 min tokens = small window of risk
Token Blocklist
Store revoked token IDs in Redis
Refresh Tokens
Short access token + long refresh token
Insight
The blocklist approach trades some of JWT's statelessness for revocation ability. You check every request against a Redis set of revoked tokens. It's fast (O(1) lookup), and you only need to store tokens until they'd naturally expire.
Understanding JWT Tokens
A JWT is three base64-encoded segments joined by dots. Explore its structure below, see how signing works, and try tampering with the payload to see what happens.
Think about it...
A JWT is base64-encoded, not encrypted. If someone intercepts a token, can they read the user's email from it?
Hint: Think about what base64 encoding actually does. Is it encryption?
Key Points
OAuth2PasswordBearer
Extracts Bearer token from Authorization header
JWT Tokens
Stateless auth — no server-side session storage needed
Swagger Integration
The /docs page gets a working Authorize button automatically
Token Expiry
Always set exp claim — short-lived tokens are more secure