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:

TokenLifetimeStoredPurpose
Access15-60 minMemory only (client)Authorize API calls
Refresh7-30 daysHttpOnly cookie or secure storageGet 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.

References


Advertisement