OAuth 2.0OIDCJWTSession TokensAccess TokenRefresh TokenAuthentication

Authentication

Master authentication patterns — OAuth 2.0 & OpenID Connect, JWT vs session tokens, token refresh strategies, and secure identity verification for modern APIs.

28 min read8 sections
01

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.

02

Auth Architecture Overview

👤

User

Provides credentials

🔐

Auth Server

Verifies identity

🎫

Token

Issued on success

🖥️

API Server

Validates token

Modern Auth Flow — High Leveltext
1. User logs in:
   ClientAuth Server (credentials: email + password, or OAuth)
   Auth Server verifiesissues tokens:
     - Access Token (short-lived: 15 min - 1 hour)
     - Refresh Token (long-lived: 7 - 30 days)

2. User makes API requests:
   ClientAPI 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:
   ClientAuth Server (with refresh token)
   Auth Server verifies refresh tokenissues new access token
   No re-login needed

4. User logs out:
   ClientAuth 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.

03

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

FeatureOAuth 2.0OpenID Connect (OIDC)
PurposeAuthorization (access delegation)Authentication (identity verification)
Answers"What can this app access?""Who is this user?"
TokenAccess Token (opaque or JWT)Access Token + ID Token (always JWT)
User infoNot included by defaultID Token contains user claims (name, email)
Use caseThird-party API access"Login with Google", SSO
Built onStandalone frameworkExtension of OAuth 2.0

Authorization Code Flow (Most Common)

OAuth 2.0 Authorization Code Flowtext
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 backendGoogle 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

FlowUse CaseHow It WorksSecurity
Authorization CodeWeb apps with backendCode exchanged server-side for tokensHigh (secret stays on server)
Auth Code + PKCESPAs, mobile apps (no backend secret)Code + code_verifier instead of client_secretHigh (PKCE prevents interception)
Client CredentialsService-to-service (no user)Service authenticates with client_id + secretHigh (server-only, no user involved)
Implicit (deprecated)Legacy SPAsToken returned directly in URL fragmentLow (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."

04

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)

JWT — Self-Contained Tokentext
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 payloadno 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

Session Token — Server-Side Statetext
Login:
  Client sends credentialsServer 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_id401 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 instantdelete the session.
FeatureJWTSession Token
StorageClient-side (localStorage, cookie)Server-side (Redis, DB)
VerificationSignature check (no DB call)Session store lookup (Redis call)
ScalabilityHigh (stateless, any server can verify)Medium (all servers need shared session store)
RevocationHard (valid until expiry, unless blocklist)Easy (delete from session store)
Token sizeLarge (~800 bytes with claims)Small (~32 bytes, just an ID)
User infoEmbedded in token (no extra call)Requires session store lookup
Security on leakAttacker has full access until expiryServer can revoke immediately
Best forMicroservices, APIs, mobile appsMonoliths, internal apps, high-security

Token Refresh & Revocation

Token Refresh Flowtext
Access token expires (after 15 minutes):

  ClientAPI: GET /api/users/me
  Header: Authorization: Bearer <expired_access_token>
  API: 401 Unauthorized (token expired)

  ClientAuth 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 token200 OK

JWT Revocation (the hard problem):
  JWTs are valid until they expireyou can't "delete" them.
  Solutions:
    1. Short expiry (15 min) — limits damage window
    2. Token blocklistcheck a Redis set of revoked JWTs on each request
       (adds a DB call, partially defeats the "stateless" benefit)
    3. Refresh token revocationrevoke 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."

05

End-to-End Scenario

Let's design the authentication system for a web application with social login, API access, and mobile clients.

Auth System — Full Designtext
System: SaaS app with web, mobile, and public API

SOCIAL LOGIN (OAuth 2.0 + OIDC):
  1. User clicks "Login with Google"
  2. Redirect to Googleuser authenticatesauthorization code
  3. Backend exchanges code for tokens (server-to-server)
  4. ID tokenextract email, namefind 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 matchissue same access + refresh tokens as above
  4. If fail401 with rate limiting (5 attempts per 15 min)

API REQUEST FLOW:
  ClientAPI Gateway
  Header: Authorization: Bearer <jwt>

  API Gateway:
    1. Decode JWT headerget key ID (kid)
    2. Fetch public key from JWKS endpoint (cached)
    3. Verify signaturevalid
    4. Check exp > nownot 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 expiresclient sends refresh token
  Auth server: verify refresh token in DBissue 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 (asymmetricservices 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.

06

Trade-offs & Decision Making

DecisionOption AOption BChoose A WhenChoose B When
Auth approachOAuth/OIDC (delegated)Custom auth (own login)Social login, SSO, third-party integrationFull control, no external dependency, simple app
Token typeJWT (stateless)Session (stateful)Microservices, APIs, mobile, horizontal scalingMonolith, instant revocation, high-security
Token storage (client)httpOnly cookielocalStorageWeb apps (CSRF protection with SameSite)Never for tokens (XSS vulnerable)
Signing algorithmRS256 (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."

07

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.

1

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.

08

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.