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.
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.
ContinueSecure Service-to-Service Communication
Replace shared API keys with workload-identity-based auth.
Continue