How to Implement JWT Authentication: Tokens, Refresh, and Security
Prerequisites
- Understanding of HTTP and REST APIs
- Basic cryptography concepts
- Python 3.11+ with FastAPI (or any framework)
What is JWT?
JSON Web Token (JWT) is a compact, URL-safe token format. It consists of three base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsImV4cCI6MTcwMDAwMDAwMH0.abc123signature
|------- Header -------|---------- Payload ----------|----- Signature -----|
Header — algorithm (HS256, RS256)
Payload — claims (user ID, expiry, roles)
Signature — prevents tampering (header + payload + secret)
Access + Refresh Token Flow
The standard pattern uses two tokens:
| Token | Lifetime | Stored | Purpose |
|---|---|---|---|
| Access | 15-60 min | Memory only (client) | Authorize API calls |
| Refresh | 7-30 days | HttpOnly cookie or secure storage | Get new access tokens |
Client Server
| |
|--- POST /login (email, pw) -->|
| |-- Validate credentials
|<-- { access_token, refresh } --|
| |
|--- GET /users (Authorization: Bearer <access>) -->|
|<-- 200 { users: [...] } ------|
| |
|--- POST /refresh (refresh_token) -->|
|<-- { access_token } --------|
Step 1: Generate tokens
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
def create_access_token(user_id: int) -> str:
payload = {
"sub": str(user_id),
"exp": datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
"iat": datetime.utcnow(),
"type": "access"
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def create_refresh_token(user_id: int) -> str:
payload = {
"sub": str(user_id),
"exp": datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS),
"iat": datetime.utcnow(),
"type": "refresh"
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
Step 2: Login endpoint
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/login")
def login(req: LoginRequest):
user = authenticate(req.email, req.password) # Your auth logic
if not user:
raise HTTPException(401, "Invalid credentials")
access = create_access_token(user.id)
refresh = create_refresh_token(user.id)
return {
"access_token": access,
"refresh_token": refresh,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
Step 3: Verify and decode tokens
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(401, "Invalid token")
# Optional: check token type
if payload.get("type") != "access":
raise HTTPException(401, "Not an access token")
except jwt.ExpiredSignatureError:
raise HTTPException(401, "Token expired")
except jwt.InvalidTokenError:
raise HTTPException(401, "Invalid token")
return user_id
@app.get("/users")
def list_users(current_user: str = Depends(get_current_user)):
return {"user_id": current_user, "users": []}
Step 4: Refresh endpoint
@app.post("/refresh")
def refresh_token(refresh_token: str):
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "refresh":
raise HTTPException(401, "Not a refresh token")
user_id = payload.get("sub")
except jwt.ExpiredSignatureError:
raise HTTPException(401, "Refresh token expired, please login again")
except jwt.InvalidTokenError:
raise HTTPException(401, "Invalid token")
return {
"access_token": create_access_token(user_id),
"token_type": "bearer"
}
Security Best Practices
1. Use RS256, not HS256 (for distributed systems)
HS256 uses a shared secret. If you have multiple services, use RS256 (asymmetric):
# RS256 — sign with private key, verify with public key
import jwt
with open("private.pem", "rb") as f:
private_key = f.read()
token = jwt.encode(payload, private_key, algorithm="RS256")
2. Keep access token lifetime short
- Access token: 15 minutes
- Refresh token: 7 days (rotated on each use)
3. Store tokens securely on the client
- Access token: In-memory variable (JS closure, React state). Never localStorage.
- Refresh token: HttpOnly, Secure, SameSite=Strict cookie.
4. Never put sensitive data in the payload
The payload is base64-encoded, not encrypted. Anyone with the token can decode it:
import base64, json
# Don't put secrets here!
payload = {"sub": "42", "email": "[email protected]"} # email is fine
# payload = {"credit_card": "4111..."} # NEVER do this
5. Implement token revocation
JWT is stateless — there’s no built-in logout. Workarounds:
- Token blacklist — store revoked token IDs in Redis until they expire
- Short access tokens — 5-15 min; revocation is near-instant
- User version field — increment a version counter on password change; reject tokens with old versions
Verification
# Get tokens
curl -X POST http://localhost:8000/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"secret"}'
# {"access_token":"eyJ...","refresh_token":"eyJ..."}
# Use access token
curl http://localhost:8000/users \
-H "Authorization: Bearer eyJ..."
# {"user_id":"1","users":[]}
# Refresh
curl -X POST http://localhost:8000/refresh?refresh_token=eyJ...
# {"access_token":"eyJ..."}
FAQ
Q: Should I use JWT for sessions? No. Use cookie-based sessions for server-rendered apps. JWT is for stateless APIs.
Q: Where do I store the refresh token? HttpOnly cookie (most secure) or a mobile keystore (iOS/Android).
Q: Can the client decode the JWT? Yes. JWT payload is public. Don’t store secrets in it.