OAuth 2.0 is an authorization framework that allows applications to obtain limited access to user accounts on third-party services without the user sharing their password. JWT (JSON Web Tokens) are self-contained, cryptographically signed tokens used for stateless authentication. Together, OAuth 2.0 and JWTs power the authentication layer of virtually every modern web application. Understanding their design — grant flows, token types, validation, and security pitfalls — is essential for system design interviews at companies with complex authentication requirements.
OAuth 2.0 Grant Flows
OAuth 2.0 defines four grant types for obtaining access tokens: Authorization Code flow (most secure, for server-side apps): redirect user to auth server → user approves → auth server returns a short-lived code → server exchanges code for access + refresh tokens (code exchange happens server-to-server, never in the browser). PKCE extension (for SPAs and mobile apps): generates a code verifier + code challenge before redirect; the code challenge is sent with the auth request; the verifier is sent with the token exchange — prevents authorization code interception attacks without a client secret. Client Credentials flow (machine-to-machine): service directly authenticates with client_id + client_secret and gets an access token. No user involved. Used for service-to-service API calls. Device Authorization flow: for devices without browsers (smart TVs, CLI tools); displays a code for the user to enter on another device.
// JWT structure: header.payload.signature (base64url encoded)
// Header: {"alg": "RS256", "typ": "JWT", "kid": "key-2024-01"}
// Payload: {
// "sub": "user_12345", // subject (user ID)
// "iss": "https://auth.example.com", // issuer
// "aud": "api.example.com", // audience (intended recipient)
// "exp": 1735689600, // expiry (Unix timestamp)
// "iat": 1735686000, // issued at
// "jti": "abc123", // JWT ID (for revocation)
// "roles": ["user", "admin"] // custom claims
// }
// Signature: RS256(base64url(header) + "." + base64url(payload), private_key)
// JWT validation (Go example)
func validateJWT(tokenStr string, jwks *JWKS) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
// Verify signing algorithm (NEVER skip — algorithm confusion attack)
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
// Get public key from JWKS by kid
kid := t.Header["kid"].(string)
return jwks.GetKey(kid)
})
if err != nil { return nil, err }
claims := token.Claims.(*Claims)
// Validate audience, issuer explicitly
if !claims.VerifyAudience("api.example.com", true) {
return nil, errors.New("invalid audience")
}
return claims, nil
}
Access Tokens vs. Refresh Tokens
Access tokens are short-lived (5-60 minutes) credentials that authorize API requests. They are stateless (no server lookup needed) when JWTs are used — the server validates the signature and expiry without any database call. Refresh tokens are long-lived (days to weeks) credentials stored securely by the client. When the access token expires, the client exchanges the refresh token for a new access token + refresh token pair (refresh token rotation — the old refresh token is invalidated). Refresh token revocation: store refresh tokens in a database or Redis; on logout, mark the token as revoked. Short access token TTLs limit the window where a stolen token is usable. For immediate revocation of access tokens, maintain a token blocklist in Redis (check on every API request with TTL matching access token expiry).
JWKS and Key Rotation
JWTs signed with asymmetric keys (RS256, ES256) use a private key to sign and a public key to verify. The auth server publishes public keys at a JWKS (JSON Web Key Set) endpoint (e.g., /.well-known/jwks.json). API servers fetch and cache the JWKS, using the kid (key ID) header in the JWT to select the right public key. Key rotation: the auth server generates a new key pair periodically (e.g., monthly), publishes both old and new public keys in JWKS during a transition period, signs new tokens with the new key, and removes the old key from JWKS after all old tokens have expired. API servers see both keys in JWKS, validate tokens with whichever key matches the kid header. Rotating keys limits the blast radius of a private key compromise.
Key Interview Discussion Points
- Algorithm confusion attack: if the server accepts both RS256 and HS256, an attacker can take an RS256-signed JWT and re-sign it with HS256 using the public key as the HMAC secret — always verify the algorithm matches exactly
- Token storage in browsers: localStorage is accessible to JavaScript (XSS risk); httpOnly cookies are not accessible to JavaScript but are sent automatically (CSRF risk, mitigate with SameSite=Strict and CSRF tokens); httpOnly + SameSite=Strict cookies are the most secure storage
- Scope and least privilege: OAuth scopes limit what an access token can do (read:profile, write:posts); grant the minimum scope required; users should be shown exactly what access is being requested
- OpenID Connect (OIDC): OAuth 2.0 extension that adds identity (who the user is) on top of authorization (what the user can access); the ID token is a JWT containing user identity claims; used by Google Sign-In, Apple Sign-In
- Token introspection: for opaque access tokens (not JWTs), resource servers call the auth server introspection endpoint to validate a token on each request — adds latency but enables immediate revocation