Authentication & Authorization
Verify who a user is (AuthN) and what they can do (AuthZ) — the security layer every system must design from the ground up, not bolt on later.
★★★★★5/5System topology — how multiple services are organised
Interactive visualization
LiveUser clicks Sign In. Client generates a random code_verifier (43–128 chars) and derives code_challenge = BASE64URL(SHA256(code_verifier)). This is PKCE — Proof Key for Code Exchange (RFC 7636).
How it works
Authentication (AuthN) answers 'who are you?' — verifying identity by checking a credential: a password, hardware key, biometric, or a signed assertion from a trusted identity provider.
Authorisation (AuthZ) answers 'what can you do?' — once identity is established, determining which resources and operations are permitted for that identity.
Four strategies dominate modern systems:
**Session-based auth (stateful):** Server creates a session record in a database or cache, sends the session ID as a cookie. Every request requires a store lookup. Simple, easy to invalidate instantly, but requires a shared session store for horizontal scaling.
**JWT / Token-based auth (stateless):** Server issues a signed JSON Web Token (header.payload.signature) containing identity claims. The client sends it on every request; the server verifies the cryptographic signature — no database lookup. Scales horizontally, but tokens cannot be revoked before expiry without maintaining a blocklist.
**OAuth 2.0:** A delegation protocol — not an authentication protocol. Lets a user grant a third-party application scoped access to their resources without sharing passwords. The Authorization Code + PKCE flow (RFC 7636) is the standard for web and mobile apps. PKCE prevents code interception attacks by binding the token exchange to the party that initiated the flow.
**OpenID Connect (OIDC):** An identity layer on top of OAuth 2.0. Adds an id_token — a signed JWT asserting who the user is. Google, GitHub, Apple, and Azure AD all expose OIDC endpoints, making it the standard for federated login ('Sign in with Google').
Project structure
recommended layoutsrc/├── auth/│ ├── pkce.ts # PKCE helpers (code_verifier + challenge)│ ├── token.ts # JWT sign, verify, refresh│ ├── oauth-client.ts # Authorization Code + PKCE flow│ └── middleware.ts # Request auth guard├── routes/│ ├── login.ts # GET /login → redirect to Auth Server│ ├── callback.ts # GET /callback → exchange code for tokens│ └── protected.ts # Routes guarded by auth middleware└── config/└── auth.ts # client_id, scopes, JWKS URL, issuer
Implementation
TypeScript · Go · Rust// auth/pkce.ts — PKCE helpers (RFC 7636)
import crypto from "crypto";
export function generateCodeVerifier(): string {
return crypto.randomBytes(64).toString("base64url").slice(0, 128);
}
export function generateCodeChallenge(verifier: string): string {
return crypto.createHash("sha256").update(verifier).digest("base64url");
}
// auth/oauth-client.ts — Authorization Code + PKCE flow
export function buildAuthorizationUrl(params: {
authEndpoint: string;
clientId: string;
redirectUri: string;
scopes: string[];
codeChallenge: string;
state: string;
}): string {
const url = new URL(params.authEndpoint);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", params.clientId);
url.searchParams.set("redirect_uri", params.redirectUri);
url.searchParams.set("scope", params.scopes.join(" "));
url.searchParams.set("state", params.state);
url.searchParams.set("code_challenge", params.codeChallenge);
url.searchParams.set("code_challenge_method", "S256");
return url.toString();
}
export async function exchangeCodeForTokens(params: {
tokenEndpoint: string;
clientId: string;
code: string;
codeVerifier: string;
redirectUri: string;
}): Promise<{ access_token: string; refresh_token: string; id_token?: string }> {
const res = await fetch(params.tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: params.clientId,
code: params.code,
code_verifier: params.codeVerifier,
redirect_uri: params.redirectUri,
}),
});
if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
return res.json();
}
// auth/token.ts — JWT verification (stateless, no DB lookup)
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json")
);
export async function verifyAccessToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
return payload; // typed: sub, scope, exp, iat, ...
}
// auth/middleware.ts — Express/Next.js auth guard
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction
) {
const auth = req.headers.authorization;
if (!auth?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing token" });
}
try {
req.user = await verifyAccessToken(auth.slice(7));
next();
} catch {
res.status(401).json({ error: "Invalid or expired token" });
}
}Why it matters
Authentication is the first line of defence for every system. Bolting it on after the fact is expensive and error-prone — broken auth is the #2 OWASP risk. Designing the identity model early (session vs token, internal vs federated) shapes API design, scalability, and compliance posture for the life of the system.
✓ When to use
- →Any system where users have accounts or different users see different data
- →APIs accessed by third-party clients — use OAuth 2.0 + PKCE
- →Microservices requiring service-to-service auth — use JWTs with short expiry
- →B2B products needing enterprise SSO — implement OIDC or SAML
- →Mobile apps — Authorization Code + PKCE, never implicit flow
✗ When NOT to use
- →Fully public read-only APIs with no user data — auth adds overhead with no benefit
- →Internal tooling behind a VPN with no external exposure may not need full OAuth
Trade-offs
JWTs are stateless — any server instance verifies without a DB call
JWTs cannot be revoked before expiry without a blocklist (adds statefulness back)
OAuth 2.0 enables delegated access without sharing credentials
OAuth 2.0 is a complex spec — wrong implementation creates severe vulnerabilities
OIDC / federated login removes password management burden
External IdP becomes a hard dependency — their outage prevents all logins
Short-lived access tokens + refresh tokens balance security and UX
Token rotation logic (refresh, revoke, re-issue) adds implementation surface area
In production
OIDC provider for 'Sign in with Google' — issues id_token + access_token via Authorization Code + PKCE
OAuth 2.0 for third-party app access; fine-grained PATs for CI/CD service accounts
OAuth 2.0 Connect for platforms; API keys with restricted scopes for server-to-server calls
STS issues short-lived JWT tokens (IAM role credentials); Cognito provides OIDC for user pools
Industry adoption
Related principles
API Gateway
LiveSingle entry point for all clients that handles routing, authentication, rate limiting, and protocol translation.
Microservices Architecture
LiveDecompose an application into small, independently deployable services that communicate over a network.
Separation of Concerns
Divide a program into distinct sections where each section addresses one concern — so changes to one area don't ripple unexpectedly into unrelated areas.