JWT Authentication

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.

Installing and Creating Tokens

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.

Decoding and the get_current_user Dependency

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

Wiring It Into the /token Endpoint

@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"}

Access + Refresh Token Pattern

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"}
Security rules: a JWT payload is only signed, not encrypted — anyone can base64-decode it, so never put passwords or PII inside. Keep 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.

Quick Questions and Answers

Q: How do I "log out" a user with JWT?

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

Q: Why did my token validation fail with "Signature verification failed" after a redeploy?

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.

Q: PyJWT or python-jose?

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.

Conclusion

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.