Authentication
Master authentication patterns — OAuth 2.0 & OpenID Connect, JWT vs session tokens, token refresh strategies, and secure identity verification for modern APIs.
Table of Contents
The Big Picture — What Is Authentication?
Authentication answers one question: "Who are you?" It's the process of verifying a user's identity — proving that the person making a request is who they claim to be. It's the first gate every request must pass through before the system decides what that person is allowed to do.
The Airport Security Analogy
Authentication is showing your passport at the airport — it proves your identity. The officer checks: is this a real passport? Does the photo match? Is it expired? Once verified, you get a boarding pass (token). Authorization is what happens next: the boarding pass says you can board Flight 42 to Tokyo, but not Flight 99 to London. You've proven WHO you are (authentication), and the boarding pass defines WHAT you can access (authorization). In software: logging in = authentication. Checking if you can delete a post = authorization.
Authentication (AuthN)
WHO are you? Verify identity via credentials (password, biometrics, OAuth). Result: a token proving your identity.
Authorization (AuthZ)
WHAT can you do? Check permissions based on your identity. Result: allow or deny access to a resource.
🔥 Key Insight
Authentication and authorization are separate concerns. You can be authenticated (proven identity) but not authorized (no permission). A junior employee has a valid badge (authenticated) but can't enter the server room (not authorized). Always design them as independent layers.
Auth Architecture Overview
User
Provides credentials
Auth Server
Verifies identity
Token
Issued on success
API Server
Validates token
1. User logs in: Client → Auth Server (credentials: email + password, or OAuth) Auth Server verifies → issues tokens: - Access Token (short-lived: 15 min - 1 hour) - Refresh Token (long-lived: 7 - 30 days) 2. User makes API requests: Client → API Server Header: Authorization: Bearer eyJhbGciOiJSUzI1NiJ9... API Server validates the access token: - JWT: verify signature locally (no DB call) - Session: look up token in session store (Redis) 3. Token expires: Client → Auth Server (with refresh token) Auth Server verifies refresh token → issues new access token No re-login needed 4. User logs out: Client → Auth Server (revoke refresh token) Refresh token deleted from store Access token still valid until expiry (JWT limitation)
Identity Provider (IdP)
The service that verifies identity and issues tokens. Can be your own auth server (Auth0, Keycloak) or a third-party (Google, GitHub). Centralizes identity management.
Stateless Auth (JWT)
The token contains all the information needed to verify the user. The API server validates the signature without calling the auth server or database. Scales horizontally.
Stateful Auth (Sessions)
The token is an opaque ID. The API server looks it up in a session store (Redis) to get user info. Easy to revoke but requires shared state across servers.
OAuth 2.0 & OpenID Connect
OAuth 2.0 is an authorization framework that lets users grant third-party apps limited access to their resources without sharing their password. OpenID Connect (OIDC) is an identity layer on top of OAuth that adds authentication — it tells you WHO the user is, not just what they can access.
The Valet Key
OAuth is like a valet key for your car. You give the valet a special key that can start the engine and drive, but can't open the trunk or glove box. You didn't give them your master key (password) — you gave them limited access (scope). 'Login with Google' works the same way: you don't give the app your Google password. Google gives the app a token with limited permissions (read your email, see your profile) — and you can revoke it anytime.
OAuth 2.0 vs OIDC
| Feature | OAuth 2.0 | OpenID Connect (OIDC) |
|---|---|---|
| Purpose | Authorization (access delegation) | Authentication (identity verification) |
| Answers | "What can this app access?" | "Who is this user?" |
| Token | Access Token (opaque or JWT) | Access Token + ID Token (always JWT) |
| User info | Not included by default | ID Token contains user claims (name, email) |
| Use case | Third-party API access | "Login with Google", SSO |
| Built on | Standalone framework | Extension of OAuth 2.0 |
Authorization Code Flow (Most Common)
User clicks "Login with Google" on your app: 1. REDIRECT TO AUTH SERVER Your app redirects user to: https://accounts.google.com/o/oauth2/auth ?client_id=YOUR_APP_ID &redirect_uri=https://yourapp.com/callback &response_type=code &scope=openid email profile &state=random_csrf_token 2. USER AUTHENTICATES User logs into Google (if not already) User sees: "YourApp wants to access your email and profile" User clicks "Allow" 3. AUTH SERVER REDIRECTS BACK Google redirects to: https://yourapp.com/callback?code=AUTH_CODE_XYZ&state=random_csrf_token 4. EXCHANGE CODE FOR TOKENS (server-to-server) Your backend → Google token endpoint: POST https://oauth2.googleapis.com/token Body: { code: AUTH_CODE_XYZ, client_id, client_secret, redirect_uri } Google responds: { "access_token": "ya29.a0AfH6SM...", // Access APIs "refresh_token": "1//0eXy7z...", // Get new access tokens "id_token": "eyJhbGciOiJSUzI1NiJ9...", // User identity (OIDC) "expires_in": 3600 } 5. YOUR APP HAS THE USER'S IDENTITY Decode id_token → { sub: "12345", email: "user@gmail.com", name: "Alice" } Create or find user in your database Issue your own session/JWT for subsequent requests Key security: the authorization code is exchanged server-to-server. The client_secret never reaches the browser.
OAuth Flows
| Flow | Use Case | How It Works | Security |
|---|---|---|---|
| Authorization Code | Web apps with backend | Code exchanged server-side for tokens | High (secret stays on server) |
| Auth Code + PKCE | SPAs, mobile apps (no backend secret) | Code + code_verifier instead of client_secret | High (PKCE prevents interception) |
| Client Credentials | Service-to-service (no user) | Service authenticates with client_id + secret | High (server-only, no user involved) |
| Implicit (deprecated) | Legacy SPAs | Token returned directly in URL fragment | Low (token exposed in URL, no refresh) |
Key Tokens
- ✅Access Token — short-lived (15 min - 1 hr), used to call APIs
- ✅Refresh Token — long-lived (7-30 days), used to get new access tokens
- ✅ID Token (OIDC) — JWT containing user identity claims
- ✅Access tokens should be short-lived to limit damage if leaked
- ✅Refresh tokens should be stored securely (httpOnly cookie, server-side)
Security Rules
- ✅Always use Authorization Code flow (not Implicit)
- ✅Use PKCE for SPAs and mobile apps
- ✅Validate the state parameter to prevent CSRF
- ✅Verify ID token signature and claims (iss, aud, exp)
- ✅Store refresh tokens server-side, never in localStorage
🎯 Interview Insight
OAuth 2.0 is the industry standard. Say: "For 'Login with Google', I'd use the Authorization Code flow with PKCE. The user authenticates with Google, we receive an authorization code, exchange it server-side for tokens, and use the ID token to identify the user. We then issue our own JWT for subsequent API calls."
JWT vs Session Tokens
After authentication, the server issues a token that the client sends with every subsequent request. The two main approaches — JWT and session tokens — represent a fundamental trade-off between scalability and control.
JWT (JSON Web Token)
Structure: Header.Payload.Signature Header (base64): { "alg": "RS256", "typ": "JWT" } Payload (base64): { "sub": "user_42", // Subject (user ID) "email": "alice@example.com", "role": "admin", "iat": 1706140800, // Issued at "exp": 1706144400 // Expires (1 hour later) } Signature: RS256(base64(header) + "." + base64(payload), private_key) Verification (any API server): 1. Split token into header.payload.signature 2. Verify signature using the auth server's PUBLIC key 3. Check exp > now (not expired) 4. Check iss = expected issuer 5. Extract user info from payload → no DB call needed Key insight: the token IS the session. All user info is embedded in the token. Any server with the public key can verify it. No shared session store needed.
Session Tokens
Login: Client sends credentials → Server verifies Server creates session in Redis: SET "session:abc123xyz" '{"user_id":"42","role":"admin"}' EX 3600 Server returns: Set-Cookie: session_id=abc123xyz; HttpOnly; Secure Subsequent requests: Client sends: Cookie: session_id=abc123xyz Server: GET "session:abc123xyz" from Redis → { user_id: "42", role: "admin" } → User is authenticated Logout: Server: DEL "session:abc123xyz" from Redis → Session immediately invalidated → Next request with this session_id → 401 Unauthorized Key insight: the token is just an ID. All user info is stored server-side (Redis). The server must look up every request. But revocation is instant — delete the session.
| Feature | JWT | Session Token |
|---|---|---|
| Storage | Client-side (localStorage, cookie) | Server-side (Redis, DB) |
| Verification | Signature check (no DB call) | Session store lookup (Redis call) |
| Scalability | High (stateless, any server can verify) | Medium (all servers need shared session store) |
| Revocation | Hard (valid until expiry, unless blocklist) | Easy (delete from session store) |
| Token size | Large (~800 bytes with claims) | Small (~32 bytes, just an ID) |
| User info | Embedded in token (no extra call) | Requires session store lookup |
| Security on leak | Attacker has full access until expiry | Server can revoke immediately |
| Best for | Microservices, APIs, mobile apps | Monoliths, internal apps, high-security |
Token Refresh & Revocation
Access token expires (after 15 minutes): Client → API: GET /api/users/me Header: Authorization: Bearer <expired_access_token> API: 401 Unauthorized (token expired) Client → Auth Server: POST /auth/refresh Body: { refresh_token: "rt_xyz789..." } Auth Server: 1. Verify refresh token exists in DB and is not revoked 2. Check refresh token not expired (30-day TTL) 3. Issue new access token (15-min TTL) 4. Optionally rotate refresh token (issue new one, revoke old) 5. Return: { access_token: "new_jwt...", refresh_token: "new_rt..." } Client retries original request with new access token → 200 OK JWT Revocation (the hard problem): JWTs are valid until they expire — you can't "delete" them. Solutions: 1. Short expiry (15 min) — limits damage window 2. Token blocklist — check a Redis set of revoked JWTs on each request (adds a DB call, partially defeats the "stateless" benefit) 3. Refresh token revocation — revoke the refresh token so no new access tokens can be issued (existing one still valid until expiry)
🎯 Choose JWT When
- Microservices (each service verifies independently)
- Mobile apps (no server-side session needed)
- Third-party APIs (token is self-contained)
- Horizontal scaling (no shared session store)
- Immediate revocation is not critical
🎯 Choose Sessions When
- Instant revocation needed (admin force-logout)
- Monolith or small service count
- High-security applications (banking, healthcare)
- Need to track active sessions per user
- Token size matters (sessions are ~32 bytes)
🎯 Interview Insight
The standard answer: "JWT for stateless API auth at scale, sessions for control and revocation. In practice, I'd use short-lived JWTs (15 min) with refresh tokens stored server-side. This gives stateless verification for most requests, with the ability to revoke by deleting the refresh token. The 15-minute window is an acceptable trade-off."
End-to-End Scenario
Let's design the authentication system for a web application with social login, API access, and mobile clients.
System: SaaS app with web, mobile, and public API SOCIAL LOGIN (OAuth 2.0 + OIDC): 1. User clicks "Login with Google" 2. Redirect to Google → user authenticates → authorization code 3. Backend exchanges code for tokens (server-to-server) 4. ID token → extract email, name → find or create user in DB 5. Issue app tokens: - Access token: JWT, 15-min expiry, contains { user_id, role } - Refresh token: opaque, 30-day expiry, stored in PostgreSQL EMAIL/PASSWORD LOGIN: 1. User submits email + password 2. Backend: bcrypt.compare(password, stored_hash) 3. If match → issue same access + refresh tokens as above 4. If fail → 401 with rate limiting (5 attempts per 15 min) API REQUEST FLOW: Client → API Gateway Header: Authorization: Bearer <jwt> API Gateway: 1. Decode JWT header → get key ID (kid) 2. Fetch public key from JWKS endpoint (cached) 3. Verify signature → valid 4. Check exp > now → not expired 5. Extract { user_id: "42", role: "admin" } 6. Forward request to backend service with user context → No database call for auth verification ✅ TOKEN REFRESH: Access token expires → client sends refresh token Auth server: verify refresh token in DB → issue new JWT Optionally rotate refresh token (revoke old, issue new) LOGOUT: Delete refresh token from DB Access token still valid for up to 15 min (acceptable trade-off) For immediate revocation: add JWT to Redis blocklist (checked on each request) SECURITY LAYERS: - Passwords: bcrypt with cost factor 12 - Refresh tokens: stored hashed in DB (not plaintext) - Access tokens: RS256 signed (asymmetric — services verify with public key) - CSRF: SameSite cookies + CSRF token for web - Rate limiting: 5 login attempts per 15 min per IP
💡 This Is How Production Auth Works
Auth0, Firebase Auth, and Keycloak all implement this pattern: OAuth/OIDC for social login, short-lived JWTs for API auth, refresh tokens for session continuity, and JWKS for distributed key verification. The specific provider varies, but the architecture is universal.
Trade-offs & Decision Making
| Decision | Option A | Option B | Choose A When | Choose B When |
|---|---|---|---|---|
| Auth approach | OAuth/OIDC (delegated) | Custom auth (own login) | Social login, SSO, third-party integration | Full control, no external dependency, simple app |
| Token type | JWT (stateless) | Session (stateful) | Microservices, APIs, mobile, horizontal scaling | Monolith, instant revocation, high-security |
| Token storage (client) | httpOnly cookie | localStorage | Web apps (CSRF protection with SameSite) | Never for tokens (XSS vulnerable) |
| Signing algorithm | RS256 (asymmetric) | HS256 (symmetric) | Microservices (services verify with public key) | Single service (simpler, shared secret) |
🎯 Interview Framework
When discussing auth, always mention: "I'd use OAuth 2.0 with OIDC for identity, short-lived JWTs (RS256, 15-min expiry) for API auth, and refresh tokens stored server-side for session continuity. This gives stateless verification at scale with the ability to revoke via refresh token deletion."
Interview Questions
Q:What is OAuth 2.0 and how does it differ from OIDC?
A: OAuth 2.0 is an authorization framework — it lets users grant third-party apps limited access to their resources without sharing passwords. It answers 'what can this app access?' but doesn't tell you WHO the user is. OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0 — it adds an ID token (JWT) containing user claims (name, email, sub). It answers 'who is this user?' Use OAuth when you need API access delegation. Use OIDC when you need user authentication ('Login with Google').
Q:JWT vs session tokens — when do you use each?
A: JWT: self-contained token with user info, verified by signature (no DB call). Best for: microservices (each service verifies independently), APIs, mobile apps, horizontal scaling. Trade-off: hard to revoke (valid until expiry). Session tokens: opaque ID, server looks up user info in a session store (Redis). Best for: monoliths, instant revocation needed, high-security apps. Trade-off: requires shared session store, adds latency per request. In practice: use short-lived JWTs (15 min) + server-side refresh tokens. This gives stateless verification with revocation capability.
Q:How do you revoke a JWT?
A: JWTs are stateless — once issued, they're valid until expiry. Three approaches: (1) Short expiry (15 min) — limits the damage window. The user's access is revoked within 15 minutes when the refresh token is deleted. (2) Token blocklist — maintain a Redis set of revoked JWT IDs (jti claim). Check on every request. This adds a DB call, partially defeating the stateless benefit. (3) Refresh token revocation — delete the refresh token so no new access tokens can be issued. The current access token remains valid until expiry (15 min max). Most systems use approach 1 + 3: short-lived JWTs with refresh token revocation.
You're designing auth for a microservices architecture with 20 services
How would you handle authentication?
Answer: JWT with RS256 signing. The auth service signs JWTs with a private key. All 20 services verify using the public key (fetched from a JWKS endpoint, cached locally). No service needs to call the auth service or a session store on every request — verification is local and stateless. Access tokens: 15-min expiry, contain user_id and roles. Refresh tokens: 30-day expiry, stored in the auth service's database. API gateway validates the JWT once and forwards user context to downstream services. This scales to 20+ services without a shared session store bottleneck.
Common Pitfalls
Storing sensitive data in JWT payload
Putting passwords, SSNs, or internal IDs in the JWT payload. JWTs are base64-encoded, NOT encrypted — anyone can decode the payload. If a JWT is intercepted or logged, all embedded data is exposed in plaintext.
✅Only store non-sensitive claims in JWTs: user_id (UUID, not sequential), role, email. Never store passwords, secrets, or PII. If you need to include sensitive data, use JWE (encrypted JWT) — but this adds complexity. Better: keep the JWT minimal and fetch sensitive data from the backend when needed.
Not handling token expiry properly
Access tokens with 24-hour or no expiry. If a token is leaked, the attacker has access for a full day (or forever). Or: no refresh token flow — when the access token expires, the user must re-login, destroying the UX.
✅Access tokens: 15 minutes max. Refresh tokens: 7-30 days, stored securely (httpOnly cookie or server-side). Implement silent refresh: when the access token expires, the client automatically uses the refresh token to get a new one — no user interaction needed. Rotate refresh tokens on each use (revoke old, issue new).
Poor OAuth configuration
Using the Implicit flow (deprecated, tokens in URL). Not validating the state parameter (CSRF vulnerability). Not verifying the ID token signature (accepting any token). Storing client_secret in frontend code (exposed to users).
✅Always use Authorization Code flow (+ PKCE for SPAs/mobile). Validate the state parameter on callback. Verify ID token signature, issuer (iss), audience (aud), and expiry (exp). Keep client_secret on the server only. Use PKCE instead of client_secret for public clients.
Ignoring refresh tokens
Only issuing access tokens with long expiry (24h+) to avoid implementing refresh. This means: leaked tokens give long access, users must re-login frequently if expiry is short, and there's no way to revoke access without a blocklist.
✅Always implement the refresh token flow. Short-lived access tokens (15 min) + long-lived refresh tokens (30 days) stored server-side. This gives: short damage window on access token leak, seamless UX (silent refresh), and revocation capability (delete refresh token). Every major auth provider (Auth0, Firebase, Okta) uses this pattern.