Home/Learn/FastAPI/JWT — Creating and Verifying Tokens

JWT — Creating and Verifying Tokens

Advanced
Auth & Security

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

sub + exp + signature: readable by all, forgeable by none
# 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.

Short access + refresh + token_version = the production answer
# 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 tokens

Key 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 Aria
1

A user reports their token was stolen — what limits the damage in your design, layer by layer?

HardZerodha
2

Why must you never trust the alg field from the JWT header?

HardMicrosoft
3

Implement "log me out of all devices" with JWTs — what state do you concede and where?

HardFlipkart

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.

Loading discussion…