JWT Token Generation and Verification¶
🎯 What You’ll Learn¶
- What JWT tokens are and why they’re used
- How to generate JWTs after successful login
- How to verify JWTs on protected routes
- How to integrate FastAPI’s OAuth2PasswordBearer for token-based access control
🧠 What Is a JWT?¶
JWT stands for JSON Web Token. It’s a compact, URL-safe string used to represent claims between two parties — typically a server and a client.
A JWT has three parts:
header.payload.signature
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbmJyaWNvIiwiaWF0IjoxNjk5ODg3MjAwfQ.abc123...
🔍 What’s inside?¶
- Header: Specifies the algorithm (e.g. HS256)
- Payload: Contains claims like
sub(subject = username),exp(expiration) - Signature: Ensures the token hasn’t been tampered with
JWTs are stateless — the server doesn’t store them. Instead, it verifies the signature on each request.
🔐 Step 1: Install JWT Library¶
pip install python-jose
We’ll use python-jose to encode and decode JWTs.
🛠️ Step 2: Create JWT Utility Functions¶
📄 core/jwt.py
from jose import JWTError, jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key" # Use a strong, random key in production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> dict | None:
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except JWTError:
return None
🔍 What’s happening?¶
create_access_token()adds an expiration and signs the tokendecode_access_token()verifies the signature and returns the payload
🔑 Step 3: Update Login to Return a JWT¶
📄 routers/auth.py
from core.jwt import create_access_token
@router.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
user = session.exec(select(User).where(User.username == form_data.username)).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token({"sub": user.username})
return {"access_token": token, "token_type": "bearer"}
🔍 Why sub?¶
substands for “subject” — it identifies the user- You’ll use it later to fetch the current user from the token
🧩 Step 4: Protect Routes with Token Verification¶
📄 core/dependencies.py
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from core.jwt import decode_access_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
payload = decode_access_token(token)
if not payload or "sub" not in payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload["sub"]
🔍 What’s happening?¶
OAuth2PasswordBearertells FastAPI to expect aBearer <token>headerget_current_user()decodes the token and extracts the username
🔐 Step 5: Use get_current_user in Protected Routes¶
📄 routers/tasks.py
from core.dependencies import get_current_user
@router.get("/", dependencies=[Depends(get_current_user)])
def get_all_tasks(...):
...
Now this route requires a valid token. If the token is missing, invalid, or expired, FastAPI returns a 401 error.
🧪 Try It in Swagger UI¶
POST /auth/login→ Get a token- Click “Authorize” in Swagger UI and paste the token
- Access protected routes like
/tasks
FastAPI automatically adds the Authorization: Bearer <token> header.
🧠 Recap¶
You now have:
| Component | Purpose |
|---|---|
| JWT token | Proves user identity |
create_access_token() |
Generates signed token |
decode_access_token() |
Verifies and extracts claims |
get_current_user() |
Injects user identity into routes |
This enables secure, stateless authentication across your API.
🧪 Practice Challenge¶
Add:
- A
GET /meendpoint that returns the current user from the token - A
token_expiredcheck that returns 401 if the token is too old - A
refresh_token()function that issues a new token with a fresh expiration