Token-Based Auth (JWT & OAuth2)
Architectural view of JWT and OAuth2: token issuance, validation, refresh flows, token storage, and securing microservice-to-microservice communication.
Token-Based Authentication at the Architecture Level
In session-based auth, the server stores session state and the client holds only a session ID cookie. In token-based auth, the server is stateless: all necessary state is embedded in a signed token that the client presents with each request. This trade-off is fundamental to scaling distributed systems — no centralized session store means any server can validate any request independently.
JWT Structure
A JSON Web Token (JWT) is a base64url-encoded, dot-separated string: `header.payload.signature`. The header specifies the algorithm (`alg`), the payload contains claims (the data), and the signature proves the token was issued by a trusted party and has not been tampered with.
// JWT Header (base64url decoded)
{
"alg": "RS256", // Algorithm: RSA + SHA-256 (asymmetric, preferred)
"typ": "JWT",
"kid": "key-2024-01" // Key ID — used to look up the public key in JWKS
}
// JWT Payload (base64url decoded)
{
"iss": "https://auth.example.com", // Issuer
"sub": "user_abc123", // Subject (user ID)
"aud": "https://api.example.com", // Audience (who should accept this token)
"iat": 1700000000, // Issued at (Unix timestamp)
"exp": 1700003600, // Expires at (iat + 1 hour)
"roles": ["read:orders", "write:orders"],
"tenant_id": "acme-corp"
}
// Signature (server-side verification pseudocode)
signature = RS256(
base64url(header) + "." + base64url(payload),
privateKey // Only the auth server holds this
)
// Full JWT (sent to client and presented on every request)
eyJhbGci... . eyJpc3Mi... . signature_bytes_base64urlJWT Pitfall: 'alg: none' Attack
A notorious JWT vulnerability allows attackers to set `"alg": "none"` in the header and remove the signature entirely. Vulnerable libraries accept this as valid if they don't enforce algorithm allowlisting. Always explicitly specify allowed algorithms in your JWT validation library and never accept `alg: none`.
OAuth2 Grant Types — Architectural View
| Grant Type | Use Case | Token Returned | Notes |
|---|---|---|---|
| Authorization Code + PKCE | Web app, mobile, SPA user login | Access + ID + Refresh tokens | Preferred for all user-facing flows |
| Client Credentials | Machine-to-machine (M2M), service accounts | Access token only | No user involved; client authenticates with client_secret |
| Refresh Token | Silently renewing short-lived access tokens | New access token (+ new refresh token) | Rotate refresh tokens on each use |
| Device Code | Input-constrained devices (TV, CLI) | Access + Refresh tokens | Polling-based; user approves on secondary device |
| Implicit (deprecated) | Legacy SPAs | Access token directly | Avoid — tokens exposed in URL fragment |
Access Token + Refresh Token Flow
Token Storage: Where to Keep Tokens
| Storage Location | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| localStorage / sessionStorage | High — JS can read it | None | Do NOT store sensitive tokens here |
| In-memory (JS variable) | Low — not persistent | None | OK for access tokens in SPA; lost on refresh |
| HttpOnly, Secure Cookie | None — JS cannot read | Present (mitigate with SameSite) | Best for refresh tokens; use SameSite=Lax or Strict |
| Backend-for-Frontend (BFF) | None — browser holds only session cookie | Mitigated at BFF layer | Gold standard for SPA security |
Service-to-Service Auth (M2M)
In microservice architectures, services must authenticate to each other, not just to users. The OAuth2 Client Credentials grant is the standard pattern: each service has a `client_id` and `client_secret` (or a certificate) and exchanges these for an access token from the auth server. The calling service attaches this token to outgoing requests; receiving services validate it the same way they validate user tokens.
// M2M Client Credentials flow — pseudocode for a microservice
async function getServiceToken(): Promise<string> {
const response = await fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.SERVICE_CLIENT_ID,
client_secret: process.env.SERVICE_CLIENT_SECRET,
scope: "orders:read inventory:write",
}),
});
const { access_token, expires_in } = await response.json();
// Cache the token until (expires_in - buffer) seconds
cacheToken(access_token, expires_in - 60);
return access_token;
}
// Call downstream service with service token
async function getInventory(itemId: string) {
const token = await getCachedOrFetchToken();
return fetch(`https://inventory-service/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` },
});
}Token Introspection vs Offline Validation
JWT validation is offline (just cryptographic verification using cached public keys) — fast and scalable. Token introspection calls the auth server's `/introspect` endpoint to check if a token is still active (catching revoked tokens in real-time) — slower but necessary for high-security scenarios. A common hybrid: validate JWT offline for every request, and periodically call introspection for long-running sessions or when a user reports a compromised account.
Interview Tip
In system design interviews, when discussing authentication, explicitly address three scenarios: (1) browser-to-API (OIDC + access token in memory or BFF pattern), (2) mobile-to-API (Authorization Code + PKCE), (3) service-to-service (Client Credentials with cached tokens). Mentioning token rotation for refresh tokens and the 'alg: none' vulnerability shows senior-level security awareness. Also mention that access tokens should be short-lived (15 min) to limit the blast radius of a leaked token, and that refresh token rotation + revocation lists catch stolen refresh tokens.