← News
/PLAYBOOK · 2026-08-11

API Security Hardening: JWT Authentication and Rate Limiting Guide

Covers JWT issuance and verification, API rate limiting against abuse, key management, and common attack-surface controls. Includes deployable Python/FastAPI samples and verification commands.

This guide covers JWT stateless auth and API rate limiting for REST and GraphQL services, with FastAPI, Redis, and Nginx configs you can deploy as-is.

Applicable Scenarios

API security applies when an organization exposes RESTful or GraphQL APIs to frontends, mobile clients, and third-party systems and needs unified authentication and access control; when APIs are abused for traffic flooding, order farming, or data scraping; and when a microservices architecture needs identity propagation and rate limiting between the gateway and services. This article uses JWT authentication and API rate limiting as the main line and provides a deployable API security configuration.

Prerequisites

How It Works

API security separates authentication from authorization and protects resources. JWT (JSON Web Token) is a stateless token of Header, Payload, and Signature; the signature makes the token unforgeable. Unlike Session, JWT needs no server-side session store and fits distributed deployments. Because it is stateless, a leaked token cannot be revoked immediately, so pair it with a short TTL, HTTPS, and key rotation. Rate limiting uses token-bucket or sliding-window algorithms to cap requests per unit time and stop APIs from being flooded or taking down the backend.

Steps

1. Correct JWT issuance and verification

# auth.py - JWT issuance and verification
import jwt, datetime

SECRET_KEY = "please-change-this-256bit-secret-key"
ALGORITHM = "HS256"

def create_token(user_id: int) -> str:
    payload = {
        "sub": str(user_id),
        "iat": datetime.datetime.now(datetime.timezone.utc),
        "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30),
        "scope": ["user:read", "user:write"],
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> dict:
    # verify=True automatically verifies the signature and exp
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

Key parameters: `exp` must be set (recommend under 30 minutes); `algorithms` must be specified explicitly rather than accepting any algorithm, to prevent algorithm-confusion attacks.

2. Wire auth and rate limiting into FastAPI

# main.py - FastAPI auth middleware + Redis rate limiting
from fastapi import FastAPI, Depends, HTTPException, Header
import redis, time

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
app = FastAPI()

def rate_limit(client_ip: str, limit: int = 60, window: int = 60):
    key = f"rl:{client_ip}:{int(time.time()) // window}"
    count = r.incr(key)
    if count == 1:
        r.expire(key, window)
    if count > limit:
        raise HTTPException(status_code=429, detail="Too many requests, please try again later")

@app.get("/api/data")
def get_data(authorization: str = Header(...)):
    client_ip = "0.0.0.0"  # In production, obtain from X-Forwarded-For or the real connection
    rate_limit(client_ip)
    token = authorization.replace("Bearer ", "")
    try:
        payload = verify_token(token)
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")
    return {"user": payload["sub"], "data": "sensitive"}

Key parameters: `limit` and `window` set the threshold (example: 60 requests in 60 seconds); `status_code=429` is the standard rate-limit response code; clients should back off and retry.

3. Key management and rotation

# Generate a strong random key (256 bit)
openssl rand -base64 32

# In production, always read from environment variables or a key management service; do not hardcode
export JWT_SECRET="$(openssl rand -base64 32)"

4. Gateway hardening (Nginx)

# Enable IP rate limiting for the /api/ prefix in Nginx, forming dual protection with the application layer
limit_req_zone $binary_remote_addr zone=api_zone:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api_zone burst=20 nodelay;
        proxy_pass http://backend:8000;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Key parameters: `rate=10r/s` is the average rate, `burst=20` allows a burst peak, `nodelay` returns 503/429 immediately for excess so the queue does not pile up and stall the backend.

5. Close common attack surfaces

# 1) Validate Content-Type; reject non-JSON requests
# 2) Input validation: length, type, whitelist regex
# 3) Error messages must not leak internal details: return a unified {code, message} rather than a stack trace
# 4) Disable CORS wildcards; allow only trusted origins
# 5) Log redaction: do not log full tokens or password fields

Verify the configuration

# 1. Start the service
uvicorn main:app --host 0.0.0.0 --port 8000

# 2. Obtain a token (assuming a /api/login issuance endpoint exists)
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/login -d '{"user":"admin"}' -H "Content-Type: application/json" | python -c "import sys,json;print(json.load(sys.stdin)['token'])")

# 3. Access a protected endpoint with the token
curl -s http://127.0.0.1:8000/api/data -H "Authorization: Bearer $TOKEN"

# 4. Access without a token should return 401
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/api/data
# Expected: 401

# 5. Rate-limit verification: send 70 consecutive rapid requests; after the 61st should return 429
for i in $(seq 1 70); do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/api/data -H "Authorization: Bearer $TOKEN"; done | sort | uniq -c

FAQ

Q1: How do you contain a leaked JWT quickly?

JWT is stateless, so you cannot revoke at the issuer. Three moves: shorten TTL to 15-30 minutes; keep a `jti` (token ID) denylist cache (store in Redis until the token expires); rotate the signing key immediately so all old tokens fail and users must re-login. For high-value APIs, introduce Refresh Tokens: short-lived Access Tokens, revocable Refresh Tokens.

Q2: Rate limit at the gateway or in the app?

Do both; roles differ. Gateway (Nginx/API gateway) limits by IP/UA, blocking most DDoS and crawler traffic and protecting the whole backend. App-layer limits by user (`user_id`/API Key) so a single legitimate account cannot hammer the API. Both together are the complete scheme. User-level limits must run after authentication, or you cannot distinguish users.

Q3: What is special about GraphQL rate limiting?

A GraphQL request can batch queries, so request-count limits are bypassed. Also cap query complexity (depth + node count)—for example depth no more than 5, nodes no more than 100 per query—and accumulate `cost` at the gateway for quota control.

Summary

API security is authentication, authorization, rate limiting, and logging working together. JWT with a short TTL and explicit algorithm binding mitigates the inherent risk of stateless tokens. Redis sliding window and Nginx `limit_req` implement user-dimension and IP-dimension limits. Read keys from environment variables or a KMS and rotate them on a schedule. Before go-live, use load tools (ab, wrk) to confirm the rate-limit threshold, and feed 401/429 counts into monitoring and alerts.