Difficulty: Advanced
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. In the context of API authentication, JWTs are used as access tokens that encode user identity and permissions. When a user logs in, the server creates a JWT containing user information, signs it with a secret key, and returns it. The client includes this token in subsequent requests, and the server verifies it without needing to look up a session in a database.
A JWT consists of three parts separated by dots: header.payload.signature. The header specifies the signing algorithm (typically HS256). The payload contains claims - key-value pairs like sub (subject/user ID), exp (expiration time), iat (issued at), and any custom data. The signature is created by signing the header and payload with a secret key, ensuring the token has not been tampered with.
In Python, the python-jose library (or PyJWT) is commonly used to create and verify JWTs. The jose.jwt.encode() function creates a token from a dictionary of claims and a secret key. jwt.decode() verifies the signature and returns the claims. If the token is expired, tampered with, or uses the wrong key, decode raises an exception.
Token expiration is critical for security. Every JWT should have an exp claim that sets when the token expires. Short-lived tokens (15-60 minutes) are more secure because a stolen token is only usable briefly. For longer sessions, use refresh tokens - a separate long-lived token that can be exchanged for new access tokens.
The secret key used to sign JWTs must be kept secret. If someone obtains your secret key, they can create valid tokens for any user. In production, use a strong random key (at least 256 bits) stored in environment variables, not hardcoded in source code. For even more security, use RS256 (asymmetric keys) instead of HS256 (symmetric key).
JWT tokens are stateless - the server does not store them. All the information needed to verify the token is contained within the token itself (plus the secret key). This is different from session-based authentication where the server maintains a session store. Stateless tokens scale better because any server instance can verify them, but they cannot be revoked individually without additional infrastructure like a token blocklist.
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
SECRET_KEY = "your-secret-key-at-least-32-characters-long"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
# Create a token
token = create_access_token({"sub": "alice", "role": "admin"})
print(token) # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Verify a token
payload = verify_token(token)
print(payload) # {"sub": "alice", "role": "admin", "exp": 1710284400}
create_access_token encodes user data with an expiration claim. verify_token decodes and validates the token. If the token is expired or tampered with, jwt.decode raises JWTError. Always use timezone-aware datetimes for expiration.
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from pydantic import BaseModel
SECRET_KEY = "super-secret-key-change-in-production"
ALGORITHM = "HS256"
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class Token(BaseModel):
access_token: str
token_type: str
fake_users = {
"alice": {"username": "alice", "password": "secret", "role": "admin"},
}
def create_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(minutes=30)
return jwt.encode({data, "exp": expire}, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = fake_users.get(username)
if not user:
raise HTTPException(status_code=401, detail="User not found")
return user
@app.post("/token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users.get(form_data.username)
if not user or user["password"] != form_data.password:
raise HTTPException(status_code=401, detail="Bad credentials")
token = create_token({"sub": user["username"], "role": user["role"]})
return Token(access_token=token, token_type="bearer")
@app.get("/me")
def read_me(user: dict = Depends(get_current_user)):
return {"username": user["username"], "role": user["role"]}
The login endpoint creates a JWT with the user's identity encoded in the 'sub' claim. get_current_user decodes the JWT, extracts the username, and looks up the user. JWTError is caught for expired or invalid tokens.
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
# Token with multiple claims
def create_token_with_claims(
user_id: int,
username: str,
role: str,
scopes: list[str],
):
now = datetime.now(timezone.utc)
payload = {
"sub": str(user_id), # Subject (user ID)
"username": username, # Custom claim
"role": role, # Custom claim
"scopes": scopes, # Custom claim (permissions)
"iat": now, # Issued at
"exp": now + timedelta(minutes=30), # Expires
"iss": "myapp.com", # Issuer
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
token = create_token_with_claims(
user_id=42,
username="alice",
role="admin",
scopes=["users:read", "users:write", "admin"],
)
# Decode to see all claims
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# {
# "sub": "42",
# "username": "alice",
# "role": "admin",
# "scopes": ["users:read", "users:write", "admin"],
# "iat": 1710280800,
# "exp": 1710282600,
# "iss": "myapp.com"
# }
JWTs can carry any JSON-serializable claims. Standard claims include sub, exp, iat, iss. Custom claims like role and scopes encode user permissions directly in the token, enabling authorization without database lookups.
JWT, JSON Web Token, jose, Claims, Expiration, Secret Key