Search Topics

Search across all FastAPI topics

GitHub

OAuth2 & JWT

Core Concept

FastAPI 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.

terminal
# 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.

auth.py
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.

main.py
@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.

main.py
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 valid

Authentication 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