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
- A running API service (examples use Python 3.9+ and FastAPI)
- Dependencies installed: `pip install fastapi uvicorn pyjwt redis`
- Familiarity with HTTP status codes and the common Authorization header format
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.