API Security Cheatsheet
Operational reference for securing HTTP APIs in production. JWT verification patterns, OAuth2 flows, secure HTTP headers, mTLS, webhook signing, and rate-limiting that holds up against the OWASP API Top 10.
Maintained by Vishal Anand.
JWT verification (the only correct way)
4 commandsjwt.verify(token, key, { algorithms: ["RS256"] })Always pin algorithms in an allowlist. Without it, libraries trust the alg in the token header - a known attack vector.
Warning: Without algorithms allowlist, an attacker can swap RS256 → HS256 with the public key as HMAC secret and forge tokens.
jwt.verify(token, key, { issuer, audience, clockTolerance: 5 })Validate iss, aud, and clock skew. clockTolerance handles cross-machine drift.
Production note: Always validate aud - a token minted for service A must not be accepted by service B. iss anchors which IdP issued the token.
JWKS rotation: cache JWKS for 1h, force refresh on kid missWhen a kid (key ID) in a JWT header isn't in your cache, refresh JWKS once before failing.
Production note: Use libraries with JWKS support (jose, PyJWT 2.x, Auth0's jwks-rsa). Manual cache management is a footgun.
Token revocation: short TTLs + denylist for emergency revokeJWT cannot be revoked once issued. Mitigate with short TTLs (5-15 min access tokens) and refresh tokens.
Warning: Long-lived JWTs (>1 hour) without refresh flow are an outage waiting to happen - once leaked, they're valid until expiry.
OAuth2 / OIDC patterns
5 commandsresponse_type=code & PKCE (S256)Authorization Code flow with PKCE. The default for SPAs and mobile apps. PKCE prevents authz code interception.
Production note: PKCE is now recommended for confidential clients too (OAuth 2.1 draft). No reason to skip it.
redirect_uri: exact-match against allowlistNever use prefix or substring matching on redirect_uri. Parse the URI and compare host:port:path exactly.
Warning: startsWith("https://app.example.com") matches "https://app.example.com.attacker.com" - leak the auth code to the attacker.
state parameter: random per-request, validated on callbackCSRF protection for the auth flow. Tie state to the user's session.
Production note: Use a HMAC-signed state with timestamp so you can verify "this state was issued by us, recently".
refresh_token: rotate on use, detect reuseIssue a new refresh token on each refresh; revoke the entire refresh chain if an old one is reused.
Production note: Refresh-token reuse detection catches stolen refresh tokens - both attacker and legit user end up logged out, but the attacker can no longer mint access tokens.
OIDC: validate id_token signature + claims (sub, aud, iss, exp, iat, nonce)OIDC id_tokens carry user identity. Validate every claim - exp prevents replay, nonce binds to the auth request.
Warning: Skipping nonce validation enables a token-substitution attack across sessions.
Security headers (every response)
6 commandsStrict-Transport-Security: max-age=31536000; includeSubDomains; preloadForce HTTPS for 1 year, including subdomains. Preload submits to browser HSTS lists.
Warning: HSTS is sticky. Test on a subdomain before enabling site-wide; rolling back requires every visiting browser to expire the policy.
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-...'Restrict where scripts/styles/images can come from. Use script hashes or nonces, never 'unsafe-inline' or *.
Production note: Roll out with Content-Security-Policy-Report-Only first to capture violations without blocking; promote once clean.
X-Frame-Options: DENY (or CSP frame-ancestors 'none')Prevent clickjacking via iframe embedding. CSP frame-ancestors supersedes XFO in modern browsers.
Production note: Send both for maximum compatibility - older browsers ignore frame-ancestors.
X-Content-Type-Options: nosniffPrevent browsers from MIME-sniffing responses. Mitigates a class of polyglot file attacks.
Production note: Pair with explicit Content-Type on every response. Static-asset pipelines should set both.
Referrer-Policy: strict-origin-when-cross-originLimit Referer header to origin (no path) when navigating cross-origin. Default in modern browsers - set explicitly anyway.
Production note: Aggressive policies (no-referrer) can break analytics; strict-origin-when-cross-origin is the production sweet spot.
Permissions-Policy: camera=(), geolocation=(), microphone=()Disable powerful browser APIs your app doesn't use. Limits the blast radius of XSS.
Production note: Audit which features your app actually uses. Most APIs need none of these - disable them all.
CORS (the rules that matter)
4 commandsAccess-Control-Allow-Origin: <exact origin> (echoed from request)For credentialed requests, echo back the request Origin only if it's in your allowlist. Add Vary: Origin so caches don't mix responses.
Warning: Browsers reject Access-Control-Allow-Origin: * combined with Allow-Credentials: true. Use exact origins.
Access-Control-Allow-Methods: GET, POST, PUT, DELETEMethods your endpoint supports. Don't list methods you don't implement.
Production note: Wildcard methods are NOT a thing in CORS - list each one explicitly.
Access-Control-Allow-Headers: Authorization, Content-TypeHeaders the browser may send on cross-origin requests. List only what your API needs.
Production note: Authorization typically needs to be allowed for token-based APIs; never wildcard headers.
Access-Control-Max-Age: 600Cache preflight for 10 minutes. Reduces preflight overhead for chatty APIs.
Warning: Browsers cap this (Chrome 2 hours, Firefox 24 hours). Don't set huge values expecting them to be honored.
mTLS for service-to-service
3 commandstls.Config{ Certificates, ClientCAs, ClientAuth: tls.RequireAndVerifyClientCert }Server config that requires client certs and validates against ClientCAs.
Production note: Authorize the peer's identity (SPIFFE ID, CN, or SAN) after the handshake - "valid cert from our CA" is not authorization.
tlsconfig.MTLSServerConfig(source, source, authorizer) // go-spiffego-spiffe SDK builds an mTLS config that reads SVIDs from the live source. Auto-rotation, no app code.
Production note: Always use the SDK's tlsconfig helpers - manual GetX509SVID() captures a snapshot and breaks rotation.
Authorization: spiffe://corp.example.com/ns/X/sa/Y allow if peer.path startswith "/ns/X/"Authorize peers by SPIFFE ID prefix, not "any cert from our CA". Push the policy into OPA/Rego for clarity.
Warning: Substring matching on SPIFFE IDs is bypassable. Always parse the URI and compare exact trust domain + path prefix.
Webhooks (signed by sender)
4 commandsX-Hub-Signature-256: sha256=<HMAC>GitHub-style webhook signature header. HMAC-SHA256 over the raw request body, with a shared secret.
Production note: Always verify against the *raw* body - JSON parsing changes whitespace and breaks the signature.
crypto.timingSafeEqual(received, expected)Constant-time signature comparison. Prevents a timing side-channel that leaks the signature byte-by-byte.
Warning: Plain == on strings short-circuits; an attacker can measure response time to forge signatures.
X-Webhook-Timestamp + tolerance window (300s)Reject requests outside a small time window to prevent replay. Stripe uses ±5 minutes.
Production note: Sign the timestamp into the HMAC so an attacker can't adjust it.
Idempotency: dedupe by signed event IDThe same event may be delivered multiple times. Dedupe on a unique ID provided by the sender.
Warning: Without dedupe, double-charges and double-side-effects are inevitable on networks with retries.
Rate limiting (that survives spoofed headers)
3 commandsTrusted edge: strip incoming X-Forwarded-For; emit a sealed headerConfigure your CDN/ingress to strip client-supplied X-Forwarded-For and emit a fresh, trusted header.
Warning: Trusting client X-Forwarded-For lets attackers cycle the value to get a fresh rate-limit bucket per request.
Per-token + per-IP buckets, separatelyApply both - per-token catches authenticated abuse, per-IP catches unauthenticated abuse from a botnet.
Production note: Token-based limits should be more generous than anonymous limits. Tie limits to billing tier where applicable.
Backend: 429 + Retry-After headerWhen limited, return 429 with Retry-After so well-behaved clients back off.
Production note: Add a Retry-After-Reset header with the absolute reset time for client UX. Echo back rate-limit headers (X-RateLimit-Limit/Remaining/Reset).
Common misconfigurations
The unsafe pattern, the replacement, and the reason the two are not equivalent in production.
Risky
// Verify a JWT
const decoded = jwt.verify(token, getKey());Hardened
// Verify a JWT (correct)
const decoded = jwt.verify(token, getKey(), {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 5,
});Why it matters: Without algorithms, the library trusts the token's alg header - letting an attacker forge tokens by switching to HS256 with the public key as HMAC secret. Without iss/aud, a token minted by a different IdP or for a different service may be accepted.
Risky
// Webhook signature check
function verify(sig, expected) {
return sig === expected;
}Hardened
// Webhook signature check (constant-time)
function verify(sig, expected) {
const a = Buffer.from(sig, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length &&
crypto.timingSafeEqual(a, b);
}Why it matters: String == short-circuits at the first mismatched byte; an attacker can measure response time to discover the signature byte by byte. timingSafeEqual compares in constant time. The provider docs (Stripe, GitHub) explicitly recommend this.
Risky
app.use(cors({
origin: '*',
credentials: true,
}));Hardened
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
app.use(cors({
origin: (origin, cb) => {
if (!origin || allowedOrigins.includes(origin)) cb(null, true);
else cb(new Error('CORS blocked'));
},
credentials: true,
}));Why it matters: Browsers reject the * + credentials combination outright. Even if they didn't, every site could read authenticated user data via cross-origin fetch. Echo back specific allowed origins; combine with Vary: Origin so caches don't leak responses.
Related learning paths
API Attack & Defense simulator
Six API security scenarios - JWT, OAuth, mass assignment, CORS, webhooks, rate limiting.
ContinueCloud Native Security Engineering
Free 16-module course covering API + machine identity end-to-end.
ContinueM2M Authentication for Service-to-Service APIs
Compare shared secrets, signed assertions, mTLS, and workload identity for service authentication.
ContinueSafe DRF API request logging
Mask secrets, bound payload capture, and set retention for investigation evidence. Logging helps detection and debugging; it does not prevent an attack.
Continue