A JWT (JSON Web Token) is a signed, self-contained token: the server issues it at login, and every later request is authenticated by verifying the signature instead of querying a session store. The previous page's /token endpoint now returns a real JWT.
pip install pyjwt
from datetime import datetime, timedelta, timezone
import jwt
SECRET_KEY = "change-me-keep-secret" # in prod: from pydantic-settings/env
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.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
Standard claims: sub (subject — usually the user id), exp (expiry — PyJWT rejects expired tokens automatically), iat (issued at). Anything else you add (roles, scopes) travels with the token.
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_error
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired", headers={"WWW-Authenticate": "Bearer"})
except jwt.InvalidTokenError:
raise credentials_error
user = users_repo.get_by_id(user_id)
if user is None:
raise credentials_error
return user
@app.get("/users/me")
def read_me(user = Depends(get_current_user)):
return user
jwt.decode verifies the signature and exp in one call; a tampered token raises InvalidTokenError.algorithms=[...] explicitly — accepting the algorithm from the token itself is the classic JWT vulnerability (the "alg: none" attack).@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
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"})
token = create_access_token(data={"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer"}
def create_refresh_token(user_id: str) -> str:
return jwt.encode({"sub": user_id, "type": "refresh",
"exp": datetime.now(timezone.utc) + timedelta(days=7)}, SECRET_KEY, algorithm="HS256")
@app.post("/token/refresh")
def refresh(refresh_token: str = Depends(oauth2_scheme)):
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])
if payload.get("type") != "refresh":
raise HTTPException(status_code=401, detail="Not a refresh token")
return {"access_token": create_access_token({"sub": payload["sub"]}), "token_type": "bearer"}
jti server-side to support revocation ("logout everywhere").SECRET_KEY in an environment variable, rotate it with a versioned kid, and prefer RS256 (asymmetric) when other services must verify tokens: they only need the public key.
A: You cannot invalidate a stateless JWT directly. Options: short expiry times (most common), a server-side blocklist of jti values checked in get_current_user, or token versioning (a ver claim compared against the user's current version, bumped on logout/password change).
A: Each instance must use the same SECRET_KEY. If it is generated randomly at startup (or differs per container), tokens issued by one worker are invalid on another. Load it from shared configuration.
A: PyJWT is the actively maintained, focused JWT library and the current recommendation. python-jose (used in older FastAPI docs) also handles JWE/JWK but has had CVEs and slower releases. If you need JWKs from an IdP, consider authlib or python-jose[cryptography] carefully.
JWT turns authentication into signature verification: issue at /token with a short exp, validate in the get_current_user dependency, keep secrets out of the payload, and add refresh tokens for long sessions. Combined with the previous page, you now have production-grade login.