JWT — Creating and Verifying Tokens
AdvancedA JWT is signed claims: jwt.encode() issues it with sub + exp, jwt.decode() verifies signature and expiry, and get_current_user turns all of it into one reusable dependency — with revocation as the honest trade-off.
Overview
A JSON Web Token is three base64url parts — header.payload.signature — where the signature (HMAC with your secret, HS256) makes the payload tamper-proof: anyone can READ the claims, nobody can CHANGE them without the key, so never put sensitive data inside. The claims that matter: sub (who), exp (when it dies — pyjwt enforces this on decode), iat, and your own additions like role. The login endpoint from the previous chapters now issues a real token, and get_current_user decodes it per request — completing the auth chain from the DI chapters. The classic interview territory is the trade-off: statelessness means a stolen token works until exp, so you keep access tokens short-lived, pair them with refresh tokens, and add a token_version claim checked against the DB when instant revocation is a hard requirement (the standard logout-everywhere design).
Issue on Login, Verify per Request
encode() with sub/exp/iat on successful login; decode() verifies signature AND expiry in one call. Expired and tampered tokens raise distinct, catchable errors — both become 401.
# pip install pyjwt
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
SECRET = "from-Settings-never-hardcoded" # settings.jwt_secret, 32+ random bytes
ALGO = "HS256"
TTL = timedelta(minutes=30)
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(email: str, role: str) -> str:
now = datetime.now(timezone.utc)
claims = {
"sub": email, # subject — whose token
"role": role, # custom claim (readable by anyone!)
"iat": now, # issued at
"exp": now + TTL, # pyjwt REJECTS after this, automatically
}
return jwt.encode(claims, SECRET, algorithm=ALGO)
def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
try:
claims = jwt.decode(token, SECRET, algorithms=[ALGO]) # verifies sig + exp
except jwt.ExpiredSignatureError:
raise HTTPException(401, "token expired",
headers={"WWW-Authenticate": "Bearer"})
except jwt.InvalidTokenError: # bad signature, malformed, wrong algo
raise HTTPException(401, "invalid token",
headers={"WWW-Authenticate": "Bearer"})
return {"email": claims["sub"], "role": claims["role"]}
@app.get("/profile")
def profile(user: dict = Depends(get_current_user)):
return user
# eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhc2hhQG5pdGsu... ← header.payload.signature
# The payload is only base64 — decodable by ANYONE (jwt.io).
# The signature is what stops modification. Secrets do NOT belong in claims.Expiry, Refresh & the Revocation Problem
Short access tokens bound the damage of theft; refresh tokens restore UX; and when "logout everywhere, now" is a requirement, a token_version claim re-introduces exactly one DB check. algorithms=[...] pinning is the security footnote people forget.
# The trade-off triangle:
# long-lived access token → great UX, stolen token valid for days ✗
# short-lived (15-30 min) → theft window small, but re-login hourly ✗
# short access + refresh → both ✓ (the standard answer)
REFRESH_TTL = timedelta(days=7)
@app.post("/token/refresh")
def refresh(refresh_token: str):
claims = jwt.decode(refresh_token, SECRET, algorithms=[ALGO])
if claims.get("kind") != "refresh": # access tokens can't refresh
raise HTTPException(401, "not a refresh token")
return {"access_token": create_access_token(claims["sub"], claims["role"]),
"token_type": "bearer"}
# Refresh tokens: httponly cookie or secure storage, rotate on use,
# store a hash server-side so THEY are individually revocable.
# Revocation — the honest answer:
# a valid JWT cannot be remotely killed; it dies at exp. When instant
# logout-everywhere is required, add a version claim:
# claims["ver"] = user.token_version # at issue time
# ...on every request, after decode:
# if claims["ver"] != db_user.token_version: # one indexed DB read
# raise HTTPException(401, "token revoked")
# "Logout all devices" / password change → UPDATE users SET token_version += 1
# (This exact design runs AiCanCode's session security.)
# Footnotes that fail audits:
# - ALWAYS pin algorithms=[ALGO] on decode — never accept the header's
# algorithm claim (the alg=none / HS256-RS256 confusion attacks)
# - HS256 = one shared secret (monolith); RS256 = private key signs,
# public key verifies (microservices verify without the signing key)
# - JWT_SECRET rotates via platform secrets; leaking it = anyone mints tokensKey Points to Remember
- 1JWT = signed claims: readable by anyone, tamper-proof via the signature
- 2jwt.decode verifies signature and exp together; pin algorithms=[...] always
- 3Short access tokens + refresh tokens is the standard UX/security balance
- 4Instant revocation needs state: token_version claim checked against the DB
Interview Questions
Sign in to ask AriaA user reports their token was stolen — what limits the damage in your design, layer by layer?
Why must you never trust the alg field from the JWT header?
Implement "log me out of all devices" with JWTs — what state do you concede and where?
Ask Aria about JWT — Creating and Verifying Tokens
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.