<arch.design/>
Principles/Authentication & Authorization
SystemArchitectureintermediate2006oauth2jwtoidcpkce

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/5
Operates at: System level

System topology — how multiple services are organised

Interactive visualization

Live
BrowserUser AgentClient AppYour AppAuth ServerGoogle / Auth0Resource ServerYour APIClick 'Sign in'GET /authorize?code_challenge=…Render login & consent UISubmit credentials + consent302 → redirect_uri?code=…&state=…Browser delivers code to appPOST /token (code + code_verifier)access_token + refresh_tokenGET /api Authorization: Bearer <token>200 OK — protected data
Step 1 / 10Click 'Sign in'

User 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).

Why PKCE?— The plain Authorization Code flow was vulnerable: a malicious app registered on the same device could receive the code via redirect URI. PKCE (RFC 7636) adds a cryptographic one-time challenge that ties the token exchange to the exact party that started the flow — even if the code is intercepted, it's useless without the verifier.

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 layout
project structure
src/
├── 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

Google

OIDC provider for 'Sign in with Google' — issues id_token + access_token via Authorization Code + PKCE

GitHub

OAuth 2.0 for third-party app access; fine-grained PATs for CI/CD service accounts

Stripe

OAuth 2.0 Connect for platforms; API keys with restricted scopes for server-to-server calls

AWS

STS issues short-lived JWT tokens (IAM role credentials); Cognito provides OIDC for user pools

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles