Back
Engineering Guide

API Authentication Guide 2026

Practitioner's reference for API authentication: JWT vs server-side sessions, OAuth 2.1 flows, API key rotation patterns, mutual TLS, and the attacks (alg=none, key confusion, replay) that secure code defends against. Written for backend engineers picking auth for new services or hardening existing ones.

Last updated: May 2026 · Reviewed by FreeDevTool security engineering team · ~6,500 words · 26-minute read
JWT Decoder + JWT GeneratorInspect any JWT or generate signed test tokens — both run client-side, never sending data to a server.

1. Picking an auth scheme — decision matrix

Every API auth decision is a tradeoff between simplicity, scale, and trust boundaries. The shortest path:

ScenarioUseWhy
Single-org, server + browser SPA, single backendHTTP-only session cookie + CSRF tokenSimplest, server-controlled revocation, no token leak via XSS.
Mobile app + backend (single org)JWT access token + opaque refresh tokenBearer token works on mobile; refresh allows rotation.
Third-party developers calling your APIOAuth 2.1 with PKCEStandard delegated authorization; user controls scope.
Internal service-to-servicemTLS, or scoped service JWTMutual cryptographic identity; no shared secrets.
Public read API with rate limitingAPI key in Authorization headerSimple, identifies caller for quota.
Microservices with central identity providerOIDC (OAuth + OpenID Connect) ID tokensStandard, vendor-neutral.
Edge / IoT with intermittent connectivityLong-lived signed tokens with crypto attestationOffline operation; verify on reconnect.

2. JWT vs server-side sessions

The single most-debated choice. Each has clear tradeoffs:

AspectServer-side sessionJWT
StorageServer (Redis, DB, memory)Client (cookie, localStorage, header)
RevocationInstant (delete from store)Token lifetime ± blacklist required
ScalingNeeds shared session storeStateless verification
Network round-tripNone (just verify signature/cookie)None (just verify signature)
CSRF protectionRequired (CSRF token + SameSite)Not needed if not in cookie
XSS exposureNone (HttpOnly cookie)Risk if stored in localStorage
Cross-origin / mobileAwkwardNative fit
Token size~30 byte cookie500 byte – 4 KB header

The 2026 consensus among security engineers:

  • Browser SPA + same-origin backend: use HttpOnly session cookies. Avoids the XSS-localStorage problem.
  • Mobile apps, third-party APIs, multi-domain: use JWTs (or OAuth-issued JWTs).
  • Hybrid: short-lived JWT access token (5–15 min) + opaque refresh token in a server-side store. Best of both — stateless during the access window, revocable via refresh-token deletion.

3. JWT — structure, algorithms, and the attacks

A JSON Web Token (RFC 7519) is three Base64URL-encoded parts joined by dots: header.payload.signature. The payload is signed but not encrypted — anyone with the token can read its contents.

Algorithm choice

AlgorithmTypeKeyWhen to use
HS256HMAC + SHA-256Shared secret (≥ 32 bytes)Single service signs and verifies its own tokens.
RS256RSA + SHA-2562048-bit RSA private keyIssuer signs; multiple consumers verify with public key.
ES256ECDSA P-256 + SHA-256256-bit ECC keySame as RS256 with smaller signatures, faster on mobile.
EdDSA / Ed25519Edwards-curve DSA256-bit Ed25519 keyModern default for new asymmetric setups (RFC 8037).
noneUnsignedNoneNever accept. Spec allows it; safe libraries reject by default.

The classic JWT attacks — and defenses

  1. alg: none. Attacker resigns the token with {"alg": "none"} and an empty signature. Vulnerable libraries accept it because the spec technically allows it. Defense: hard-code an allow-list of algorithms; reject "none" explicitly.
  2. Algorithm confusion (RS256 → HS256). Server is configured for RS256 but accepts any algorithm. Attacker takes the public key (which is, well, public) and uses it as the HMAC secret with HS256. The signature now verifies because the library treats the public key as a shared secret. Defense: tie the algorithm to the key type — never let HS algorithms verify with an RSA public key.
  3. Key confusion via jku/jwk headers. Attacker sets a header pointing the server at an attacker-controlled JWKS URL. Defense: never trust jku/jwk/x5u/kid values from the token; resolve keys from a server-side allow-list.
  4. Long-lived tokens. Leaked tokens give permanent access if they never expire. Defense: short-lived access (5–60 min) + refresh tokens.
  5. Replay across services. Token signed for service A is replayed against service B (which trusts the same issuer). Defense: enforce aud (audience) claim validation.
  6. Information leakage. Sensitive PII in payload because "it's signed". JWTs are not encrypted. Defense: keep payload minimal; use JWE (encrypted JWT) for confidential data.
  7. Stale clock skew. exp validation fails because client and server clocks drift. Defense: allow ±30–60 seconds of clock skew tolerance in verification.
Test JWTs locally without sending to a serverUse the JWT Generator to create test tokens with any algorithm/claims, and the JWT Decoder to inspect them. Both run entirely in your browser via Web Crypto API.

Verification done right (Node.js example)

const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, secret, {
  algorithms: ['HS256'],            // explicit allow-list — defends alg=none
  issuer: 'https://issuer.example.com',
  audience: 'my-service',
  clockTolerance: 60                // ±60s for clock skew
});

4. OAuth 2.1 — the four flows you need

OAuth 2.1 (RFC 9700, draft as of 2026) consolidates the OAuth 2.0 best practices that emerged over a decade. It deprecates the implicit flow, requires PKCE for all public clients, and tightens redirect-URI matching.

Authorization Code Flow with PKCE

The default for most cases. Used by mobile apps, SPAs, server-rendered apps. PKCE (Proof Key for Code Exchange) prevents authorization-code interception attacks.

1. Client generates a random code_verifier (43-128 chars).
2. Client computes code_challenge = SHA256(code_verifier), Base64URL.
3. Client redirects user to /authorize with code_challenge.
4. User authenticates, authorizes scope.
5. Authorization server returns code to redirect_uri.
6. Client exchanges code + code_verifier for access token at /token.
7. Authorization server verifies SHA256(code_verifier) == code_challenge.
8. Returns access_token + refresh_token.

Client Credentials Flow

For machine-to-machine where there is no user — service A calling service B. Client sends its own credentials directly to /token and gets a token scoped to its identity.

Device Authorization Flow

For devices without browsers — TVs, IoT, CLI tools. Server returns a code + URL; user opens URL on a phone, types the code, approves; original device polls until token issued.

Refresh Token Flow

Access tokens are short-lived (5–60 min). When they expire, the client exchanges its refresh token for a new access token. Implement refresh token rotation: each refresh issues a new refresh token; the old one becomes invalid. Detects token theft when a stolen refresh is used after the legitimate one.

OpenID Connect (OIDC)

OIDC layers identity on top of OAuth. The authorization endpoint returns an id_token (JWT) alongside the access token, identifying who the user is. Use OIDC when you need authentication, not just authorization. Most identity providers (Auth0, Okta, Azure AD, Google) speak OIDC.

5. API keys — rotation and scoping

API keys are simpler than OAuth. They identify and authenticate the caller in one credential. Use them for:

  • Server-to-server calls within a single organization.
  • Public read APIs that need rate limiting per caller.
  • Webhooks where the caller authenticates by including a shared secret.

The patterns that make API keys safe:

  1. Always over HTTPS. Keys in HTTP requests are visible to network observers.
  2. Send in Authorization header, not URL parameters. URL params leak into logs, browser history, Referer headers.
  3. Scope keys narrowly. Read-only vs read-write, per-resource, per-environment. Stripe's restricted keys are a model.
  4. Rotate regularly. Every 90 days minimum. Shorter for high-value keys.
  5. Support multiple active keys per principal. Lets you rotate without downtime.
  6. Hash keys at rest. Treat API keys like passwords — store SHA-256 of the key, not the key itself.
  7. Log key usage. Audit access patterns; alert on anomalies (geographic, traffic spike, new IP).
  8. Generate keys cryptographically. Use the Password Generator for high-entropy keys, or crypto.randomBytes(32). Pair with a prefix like sk_live_ for visibility.
  9. Use a hash to identify keys without exposing them. Store first 8 chars + hash; show sk_live_a1b2c3... in dashboards.

Use the Hash Generator to verify key hashes match expected values during debugging. Generate cryptographically random keys with the Password Generator set to 32 characters with full charset.

6. Mutual TLS — service-to-service auth

mTLS (RFC 8705 for OAuth) extends the normal TLS handshake with client certificates. The server verifies the client's certificate against a trusted CA before accepting the connection. The result: cryptographic proof of client identity at the connection level, no shared secrets, no tokens to leak.

When mTLS makes sense:

  • Internal microservices in a zero-trust network.
  • High-value B2B integrations (payments, healthcare).
  • Service mesh setups (Istio, Linkerd, Consul Connect handle mTLS automatically).
  • Compliance regimes that require strong mutual authentication.

The challenge: certificate distribution and rotation. Most teams that adopt mTLS use a service mesh (Istio, Linkerd) or a private CA (HashiCorp Vault, AWS Private CA, cert-manager on Kubernetes) to automate cert lifecycle.

7. Production patterns — refresh tokens, idempotency, audit

Refresh token rotation

Pattern that catches token theft:

  1. Every refresh-token use issues a new refresh token AND invalidates the old one.
  2. If anyone uses the old refresh token after rotation, treat as theft signal — invalidate the entire token family.
  3. Store refresh tokens with a "family ID" so you can revoke all derivatives if one is compromised.

Idempotency keys

For state-changing operations (POST), include an Idempotency-Key header (UUIDv4 or v7). Server stores the response keyed by this value. Retries with the same key return the cached response instead of double-charging. Stripe, Square, Adyen, Paddle, and modern payments APIs support this. Generate idempotency keys with the UUID Generator.

Audit logging

For every authenticated request, log:

  • Timestamp (use the Unix Timestamp Converter for log analysis).
  • Caller identity (subject claim, API key prefix, mTLS DN).
  • Action and resource.
  • Success/failure.
  • Source IP, user agent.
  • Correlation ID (UUID for distributed trace).

Rate limiting

Per-key, per-IP, per-user. Return 429 Too Many Requests with Retry-After header. Use a sliding-window or token-bucket algorithm; fixed-window has burst issues.

8. Common security mistakes

  • Storing JWTs in localStorage. Vulnerable to XSS. Use HttpOnly cookies for browsers; Authorization header for non-browser clients.
  • Verifying JWT with jwt.verify(token, secret) only. Without algorithm allow-list, this opens alg=none. Always pass algorithms: [...].
  • Treating JWT logout as "delete the cookie". A copy of the token may exist in browser memory, in monitoring, in a logger. Real logout requires server-side blacklist or short expiry + refresh rotation.
  • Hard-coding secrets in client code. Anyone with the secret can forge tokens. Use env vars on the server only.
  • Using HS256 with a 12-character secret. Minimum security level requires the secret to be at least as long as the hash output (32 bytes for HS256).
  • Skipping issuer/audience validation. Just calling jwt.verify without checking iss and aud opens cross-service replay.
  • Accepting OAuth tokens without checking the issuer. Validate the JWKS URL against your allow-list.
  • Putting passwords in the JWT payload. Even hashed. Use the subject claim and look up via DB instead.
  • API keys in URL parameters. Logged everywhere. Always use Authorization header.
  • Forgetting to rotate keys after employee departures. Treat API keys like passwords — rotate when someone with access leaves.
  • OAuth client secret in mobile/SPA code. Public clients cannot keep secrets. Use PKCE and treat them as such.
  • Trusting the kid header for key lookup. Resolve from a server-side map; never construct file paths or URLs from token-controlled values.

9. Authoritative references

Auth tools referenced in this guide

Browser-based, never sends data to a server.