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).
Refresh token flow
when access token expiresAccess tokens are short-lived (~15 min). When one expires, the client silently exchanges the long-lived refresh token for a new pair — no user interaction required.
The access token's exp claim is in the past. Resource Server rejects the request with 401. Client detects this and starts the silent refresh — no user interaction needed.
Every successful refresh issues a brand-new refresh token and immediately invalidates the old one. Reuse of a revoked token is treated as theft — all tokens for that user are revoked.
Refresh tokens have a max lifetime (e.g. 30 days) and an idle timeout (e.g. 7 days of inactivity). Continuous usage resets the idle clock; absolute expiry forces re-authentication.
Never store refresh tokens in localStorage (XSS risk). Server-side apps store them in encrypted sessions. SPAs use short-lived memory and rely on HttpOnly cookies for silent refresh via the Auth Server's SSO session.
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
Frameworks & libraries
5 coveredYou rarely implement auth from scratch — here are the tools teams actually use.
Better Auth
TypeScript-first, self-hosted authentication with full schema ownership
Auth.js (NextAuth v5)
The most popular authentication library for Next.js and the JS ecosystem
Clerk
Managed authentication with prebuilt UI components — zero infrastructure
Lucia
Minimal session management primitives — you control the auth logic
Passport.js
The original Node.js authentication middleware — 500+ strategies
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.