apis · level 3

API Auth Patterns

API keys, bearer tokens, OAuth 2 PKCE, and JWT anatomy.

200 XP

API Authentication Patterns

Authentication tells the server who is making the request. Authorization is what that identity is allowed to do. This level covers the credentials, not the policy.

Analogy

API authentication is like the different credentials you carry into a hotel. An API key is the plastic room card — whoever is holding it gets in, no questions asked, and if you drop it in the lobby anyone can use it until the front desk deactivates that specific card. A bearer token is a paper wristband the front desk issues after you present ID at check-in; it expires at checkout time and anyone with the same wristband looks identical to the staff. OAuth with PKCE is the valet ticket system: you hand the valet a torn-off stub, they keep the matching half, and nobody can claim your car without the exact matching tear — the stub alone is useless without the half that never left your pocket. A JWT is a wristband that carries your room number, checkout date, and permissions printed right on it, sealed with a tamper-evident security strip — staff can read it at a glance without phoning the front desk, but they still check the strip is unbroken before honouring it.

API keys

An API key is a long random string issued once and included with every request. Simple, widely supported, and easy to rotate.

Where to put them:

Authorization: ApiKey sk_live_abc123…      ← preferred
X-Api-Key: sk_live_abc123…                 ← common alternative

Where never to put them:

GET /data?api_key=sk_live_abc123…          ← keys in URLs end up in logs, browser history,
                                              Referer headers, and server access logs

URL-based keys are in server access logs before you even write any code. Use headers.

Scoping and rotation: issue separate keys per client with minimal scopes. If a key leaks, revoke only that key. Keys should be long enough to resist brute force (128 bits of entropy minimum).

Bearer tokens

The Authorization: Bearer <token> pattern sends an opaque or structured token. The server validates it on every request.

GET /me HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…

Bearer tokens are what OAuth 2.0 returns after the authorization flow. They are usually JWTs (explained below) but can be opaque strings the server looks up in a database.

OAuth 2.0 — authorization code with PKCE

OAuth 2.0 is not an authentication protocol; it is an authorization protocol that lets a user grant a third-party application access to their data without sharing their password. The most secure flow for public clients (SPAs, mobile apps) is authorization code with PKCE.

The actors:

  • Resource owner — the user who owns the data
  • Client — your application
  • Authorization server — the identity provider (Auth0, Okta, Google)
  • Resource server — the API holding the data

The flow:

  1. Your app generates a random code_verifier (43–128 random characters).
  2. Compute code_challenge = BASE64URL(SHA256(code_verifier)).
  3. Redirect the user to the authorization server with code_challenge and code_challenge_method=S256.
  4. The user authenticates and approves the scopes.
  5. The authorization server redirects back with a short-lived code.
  6. Your app exchanges code + code_verifier (not the hash — the original) for an access token.
  7. The authorization server recomputes the hash and verifies it matches what was sent in step 3.
  8. If it matches, it issues an access token (and optionally a refresh token).

PKCE prevents authorization code interception attacks: even if someone intercepts the code in the redirect URL, they cannot exchange it without the code_verifier, which never left your app.

The access token is what you include in Authorization: Bearer headers going forward.

JWT anatomy

JSON Web Tokens (JWTs) are a compact, self-contained format for access tokens. A JWT is three Base64URL-encoded strings joined by dots:

<header>.<payload>.<signature>

Header:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-2024-01"
}
  • alg — the signing algorithm (RS256, ES256, HS256)
  • typ — always JWT
  • kid — key ID, used to look up the public key when verifying

Payload (standard claims):

{
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "sub": "user_abc123",
  "exp": 1735689600,
  "iat": 1735686000,
  "jti": "tok_xyz789",
  "scope": "read:users write:orders"
}
Claim Name Purpose
iss Issuer Who created this token
aud Audience Who this token is for — reject tokens not addressed to you
sub Subject The user or entity the token represents
exp Expiry Unix timestamp — reject tokens past this time
iat Issued at When the token was created
jti JWT ID Unique token identifier — enables revocation lists

Signature:

The signature is computed over BASE64URL(header) + "." + BASE64URL(payload) using the private key. The resource server verifies it using the public key (fetched from the authorization server's JWKS endpoint).

A JWT is not encrypted by default — the payload is Base64URL-encoded and readable by anyone. Do not put secrets in JWT payloads unless you use JWE (JSON Web Encryption).

The alg: none bug

Early JWT libraries accepted "alg": "none" in the header, meaning no signature. An attacker could forge any payload by setting the algorithm to none and omitting the signature. The library would accept it as valid.

Modern libraries reject alg: none by default. Always pin the accepted algorithms when verifying JWTs — never accept whatever the token claims.

// Wrong: trusts the header
jwt.verify(token, publicKey);

// Right: pins the algorithm
jwt.verify(token, publicKey, { algorithms: ["RS256"] });

Session cookies vs bearer tokens

Both patterns are valid. The right choice depends on who the client is.

Session cookies Bearer tokens
Sent automatically Yes (browser) No — must attach header
CSRF exposure Yes — needs protection No — not sent automatically
Storage HttpOnly cookie (safe) localStorage / memory
Revocation Delete server-side session Refresh token rotation or short expiry
Cross-domain Harder Easy
Mobile / non-browser Awkward Natural

For browser-based apps calling their own backend on the same domain: session cookies with HttpOnly and SameSite=Strict are a strong choice.

For public APIs, SPAs calling cross-origin backends, mobile apps: bearer tokens via Authorization header.

Do not store bearer tokens in localStorage on high-security apps — a single XSS vulnerability exposes them. Store short-lived access tokens in memory and refresh tokens in HttpOnly cookies.

Tools in the wild

6 tools
  • Hosted OIDC/OAuth provider with social logins, MFA, and rules; the SaaS default.

    service
  • Drop-in user management for React/Next.js — sessions, orgs, MFA, magic links.

    service
  • Enterprise auth (SSO/SCIM/Directory Sync) packaged as a clean REST API.

    service
  • Keycloakfree tier

    Open-source identity provider — OAuth2, OIDC, SAML; the on-prem standard.

    library
  • jwt.iofree tier

    Browser JWT inspector that decodes header/payload and verifies signatures.

    service
  • Hydra (Ory)free tier

    OAuth2 + OIDC server you can self-host alongside your own user store.

    library