Low Level Design: OAuth2 and OIDC Implementation

OAuth2 is an authorization framework that allows users to grant third-party applications limited access to their accounts without sharing passwords. OpenID Connect (OIDC) extends OAuth2 with authentication: it proves who the user is, not just what they can access. Together, OAuth2 + OIDC power every “Sign in with Google” button, every API key authorization flow, and every SSO implementation in modern software.

OAuth2 Authorization Code Flow

The authorization code flow (with PKCE) is the standard for web and mobile apps. Steps: (1) The client generates a code_verifier (random 32+ byte string) and code_challenge (SHA-256 of the verifier). (2) The client redirects the user to the authorization server: GET /authorize?response_type=code&client_id=X&redirect_uri=Y&scope=openid+email&state=CSRF_token&code_challenge=Z&code_challenge_method=S256. (3) The user authenticates and approves the scope. (4) The authorization server redirects back with an authorization code: GET /callback?code=AUTH_CODE&state=CSRF_token. (5) The client exchanges the code for tokens: POST /token with code, code_verifier, client_id, redirect_uri. (6) The authorization server returns access_token, id_token (JWT), refresh_token.

JWT Structure and Validation

A JWT (JSON Web Token) is a base64url-encoded header.payload.signature. Header: {“alg”:”RS256″,”kid”:”key_id”}. Payload (claims): {“sub”:”user_id”,”iss”:”https://auth.example.com”,”aud”:”client_id”,”exp”:1234567890,”iat”:1234567000,”email”:”user@example.com”}. Signature: RSA-SHA256 of the header.payload using the authorization server’s private key. Validation: decode the header, fetch the public key from the authorization server’s JWKS endpoint (cache the JWKS; rotate when kid changes), verify the signature, check exp (not expired), iss (expected issuer), aud (expected audience), and nonce (for OIDC flows). Never skip signature verification — an unsigned JWT is trivially forgeable.

Token Storage Security

Browser storage options for tokens: localStorage (persistent, accessible to JavaScript — vulnerable to XSS attacks; any injected script can steal tokens), sessionStorage (cleared on tab close, still accessible to JavaScript — same XSS risk), and HttpOnly cookies (not accessible to JavaScript — immune to XSS; vulnerable to CSRF unless SameSite=Strict/Lax is set). Best practice: store access tokens in memory (JavaScript variable) for single-page applications — no persistence, but XSS can still steal from memory. Store refresh tokens in HttpOnly, SameSite=Strict, Secure cookies — they survive page refreshes without being accessible to scripts. For native apps, use the OS keychain.

Refresh Tokens and Token Rotation

Access tokens are short-lived (15 minutes to 1 hour). When an access token expires, the client uses the refresh token to get a new access token without re-authenticating the user. Refresh token rotation: each time a refresh token is used, the authorization server issues a new refresh token and invalidates the old one. If a stolen refresh token is used, the legitimate client’s next refresh attempt fails — triggering a forced re-authentication. The authorization server detects theft: if both the stolen token and the legitimate token are used within the same refresh cycle (reuse detection), the server invalidates all tokens for that session. Implement refresh token families (a linked chain of refresh tokens) to enable complete session revocation.

Scopes and Claims

Scopes define what the access token permits: openid (OIDC — include user identity), email (user’s email address), profile (name, picture), read:orders (read the user’s orders), write:orders (create orders). The client requests scopes; the user approves (or the server auto-approves for first-party clients); the access token is scoped to the approved set. Claims in the JWT payload carry the approved data. Custom claims: add application-specific data to the token — tenant_id, user_role, subscription_tier. Keep tokens small — large tokens add latency to every API call (they are sent in every HTTP Authorization header). Move infrequently accessed data out of tokens and into the user info endpoint.

Client Credential Flow

The client credential flow authenticates machine-to-machine (M2M) without a user. The service presents its client_id and client_secret (or a signed JWT with a certificate) to the authorization server and receives an access token. Use for: backend service-to-service API calls, batch jobs, CI/CD pipelines. Never use client credentials flow in browser-side code — the secret would be exposed. Rotate client secrets regularly; use short-lived access tokens (15 minutes); cache the access token and reuse it until expiry (avoid requesting a new token on every API call). For highest security, replace client_secret with private key JWT authentication (RFC 7523) — the service proves its identity by signing an assertion with its private key.

Token Revocation and Introspection

JWTs are self-contained: once issued, they are valid until exp without any server check. This means a stolen JWT is valid until it expires. Revocation options: short TTL (15-minute access tokens limit the damage window), blocklist (maintain a Redis set of revoked token IDs; check on every request — adds latency but enables immediate revocation), and opaque tokens (tokens are not JWTs; the resource server calls the authorization server’s /introspect endpoint on every request to validate — always fresh but high latency). For logout, revoke the refresh token (prevents issuance of new access tokens) and let existing access tokens expire naturally (or maintain a short-lived revocation cache).

Scroll to Top