OAuth 2.0 Deep Dive
OAuth 2.0 Overview
Section titled “OAuth 2.0 Overview”OAuth 2.0 is an authorization framework defined in RFC 6749. It allows a third-party application To obtain limited access to a user’s resources on a resource server without sharing the user’s Credentials. OAuth 2.0 is not an authentication protocol — it delegates authorization.
| Role | Description |
|---|---|
| Resource Owner | The user who owns the data (e.g., a Google account holder) |
| Client | The application requesting access (e.g., a mobile app) |
| Authorization Server | Issues access tokens after authenticating the user and obtaining consent |
| Resource Server | The API that holds the protected resources (e.g., Google API) |
flowchart LR
A[Resource Owner<br/>User] --> B[Client<br/>App]
B --> C[Authorization Server]
C --> B
B --> D[Resource Server<br/>API]Grant Types
Section titled “Grant Types”Authorization Code Grant (with PKCE)
Section titled “Authorization Code Grant (with PKCE)”The most secure grant type. The client redirects the user to the authorization server, the user Authenticates and consents, and the authorization server redirects back with an authorization code. The client then exchanges the code for tokens via a back-channel request.
sequenceDiagram
participant U as User
participant C as Client App
participant A as Auth Server
participant R as Resource Server
U->>C: Click "Login with Google"
C->>A: Redirect to /authorize?response_type=code&client_id=xxx&redirect_uri=xxx&scope=openid+profile&code_challenge=xxx&code_challenge_method=S256&state=abc123
A->>U: Show login/consent page
U->>A: Authenticate and consent
A->>C: Redirect to redirect_uri?code=AUTH_CODE&state=abc123
C->>A: POST /token (code, client_id, code_verifier, redirect_uri)
A->>C: {access_token, id_token, refresh_token}
C->>R: GET /api/user (Authorization: Bearer <token>)
R-->>C: User dataPKCE (Proof Key for Code Exchange)
Section titled “PKCE (Proof Key for Code Exchange)”PKCE (RFC 7636) prevents authorization code interception attacks. It is mandatory for public clients (native apps, SPAs) and recommended for all clients.
// Generate PKCE verifier and challengefunction generatePKCE() { // Step 1: Generate a cryptographically random code_verifier (43-128 chars) const array = new Uint8Array(32); crypto.getRandomValues(array); const codeVerifier = base64URLEncode(array);
// Step 2: Derive code_challenge from code_verifier // SHA-256 hash, then base64url encode const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); crypto.subtle.digest('SHA-256', data).then((hash) => { const codeChallenge = base64URLEncode(new Uint8Array(hash)); });
return { codeVerifier, codeChallenge };}
function base64URLEncode(buffer) { return btoa(String.fromCharCode(...buffer)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '');}
// Step 3: Send code_challenge in authorization request// Step 4: Send code_verifier in token request (server verifies challenge)| Step | What Happens |
|---|---|
| 1. Generate verifier | Client creates random code_verifier |
| 2. Derive challenge | Client hashes verifier with SHA-256 → code_challenge |
| 3. Authorization | Client sends code_challenge to auth server |
| 4. Token exchange | Client sends code_verifier to auth server |
| 5. Verification | Server verifies SHA256(verifier) == challenge |
Authorization Code Flow Step-by-Step
Section titled “Authorization Code Flow Step-by-Step”1. Authorization Request (browser redirect): GET /authorize? response_type=code &client_id=my_app_id &redirect_uri=https://myapp.com/callback &scope=openid profile email &state=abc123 &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256
2. User authenticates and consents
3. Authorization Response (redirect): https://myapp.com/callback? code=SplxlOBeZQQYbYS6WxSbIA &state=abc123
4. Token Request (server-to-server, POST): POST /token Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code &code=SplxlOBeZQQYbYS6WxSbIA &client_id=my_app_id &client_secret=my_app_secret &redirect_uri=https://myapp.com/callback &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
5. Token Response: { "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "id_token": "eyJhbGci..." }Client Credentials Grant
Section titled “Client Credentials Grant”Machine-to-machine authentication. No user involved:
curl -X POST https://auth.example.com/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=service_account" \ -d "client_secret=service_secret" \ -d "scope=read:users write:users"Device Code Grant
Section titled “Device Code Grant”For devices with limited input capability (CLI tools, IoT devices, smart TVs):
1. POST /device/authorize Response: { "device_code": "GmRhmhXhwThko6mVU...", "user_code": "WDJB-MJHT", "verification_uri": "https://example.com/device", "expires_in": 1800, "interval": 5 }
2. User visits verification_uri and enters user_code
3. Client polls POST /token with device_code until user completes authorizationDeprecated Grants
Section titled “Deprecated Grants”| Grant | Why Deprecated | Replacement |
|---|---|---|
| Implicit | Token exposed in URL fragment | Authorization Code + PKCE |
| Resource Owner Password | Credentials shared with client | Authorization Code + PKCE |
Access Tokens
Section titled “Access Tokens”JWT vs Opaque Tokens
Section titled “JWT vs Opaque Tokens”| Format | Self-Contained | Revocation | Performance | Use Case |
|---|---|---|---|---|
| JWT | Yes | Difficult (short TTL) | Fast (no introspection needed) | Stateless APIs, microservices |
| Opaque | No | Easy (delete from store) | Requires introspection per request | APIs with strict revocation needs |
JWT Structure
Section titled “JWT Structure”eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. # Header (base64url)eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # Payload (base64url)SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # SignatureCommon JWT Claims
Section titled “Common JWT Claims”| Claim | Meaning | Example |
|---|---|---|
iss | Issuer | https://auth.example.com |
sub | Subject (user ID) | user-12345 |
aud | Audience (intended recipient) | https://api.example.com |
exp | Expiration time | 1700000000 |
iat | Issued at | 1700000000 |
jti | JWT ID (unique identifier) | abc123def456 |
scope | Granted scopes/permissions | read:users write:orders |
Token Validation Checklist
Section titled “Token Validation Checklist”def validate_jwt(token, expected_issuer, expected_audience, jwks_uri): # 1. Verify signature using JWKS from the authorization server jwks = fetch_jwks(jwks_uri) public_key = jwks.get_key(token.header['kid']) verify_signature(token, public_key)
# 2. Verify issuer assert token.payload['iss'] == expected_issuer
# 3. Verify audience assert token.payload['aud'] in [expected_audience]
# 4. Verify expiration assert token.payload['exp'] > time.time()
# 5. Verify not-before (if present) if 'nbf' in token.payload: assert token.payload['nbf'] <= time.time()
# 6. Verify issuer-signed claims (e.g., email_verified) return token.payloadRefresh Tokens
Section titled “Refresh Tokens”Token Rotation
Section titled “Token Rotation”Refresh token rotation (RFC 6749 Section 6) issues a new refresh token every time the old one is Used. The old token is immediately invalidated:
1. POST /token with refresh_token=ABC Response: { access_token: "new_access", refresh_token: "XYZ" }
2. ABC is now invalid. Use XYZ for the next refresh.3. If ABC is used again, ALL tokens for this user are revoked (theft detected).Revocation (RFC 7009)
Section titled “Revocation (RFC 7009)”# Revoke a specific tokencurl -X POST https://auth.example.com/revoke \ -H "Authorization: Basic base64(client_id:client_secret)" \ -d "token=eyJhbGci..."
# Revoke a refresh tokencurl -X POST https://auth.example.com/revoke \ -H "Authorization: Basic base64(client_id:client_secret)" \ -d "token=refresh_token_value&token_type_hint=refresh_token"Token Introspection (RFC 7662)
Section titled “Token Introspection (RFC 7662)”# Check if a token is active and get its metadatacurl -X POST https://auth.example.com/introspect \ -H "Authorization: Basic base64(client_id:client_secret)" \ -d "token=eyJhbGci..."
# Response:# {# "active": true,# "sub": "user-12345",# "scope": "read:users write:orders",# "exp": 1700000000,# "iat": 1699996400,# "client_id": "my_app_id"# }OpenID Connect (OIDC)
Section titled “OpenID Connect (OIDC)”OIDC is an identity layer on top of OAuth 2.0, defined by OpenID Connect Core 1.0. It provides Authentication (verifying who the user is) in addition to authorization.
OIDC Endpoints
Section titled “OIDC Endpoints”| Endpoint | Purpose |
|---|---|
/authorize | Authentication request (same as OAuth 2.0) |
/token | Token exchange (same as OAuth 2.0) |
/userinfo | Get user profile information |
/jwks | JSON Web Key Set (public keys for JWT verification) |
/.well-known/openid-configuration | OIDC discovery document |
id_token
Section titled “id_token”The id_token is a JWT that contains identity claims about the authenticated user:
{ "iss": "https://auth.example.com", "sub": "user-12345", "aud": "my_app_id", "exp": 1700000000, "iat": 1699996400, "email": "alice@example.com", "email_verified": true, "name": "Alice Smith", "picture": "https://example.com/alice.jpg"}OIDC Scopes and Claims
Section titled “OIDC Scopes and Claims”| Scope | Claims Returned |
|---|---|
openid | sub (required for all OIDC flows) |
profile | name``family_name``given_name``picture |
email | email``email_verified |
address | address (formatted address object) |
phone | phone_number``phone_number_verified |
OIDC Flows
Section titled “OIDC Flows”Authorization Code Flow: Most secure, for web apps and native apps.
Hybrid Flow: Returns tokens from the authorization endpoint and the token endpoint. Rarely used; Complex and provides minimal benefit over pure authorization code flow.
Implicit Flow (deprecated): Returned tokens directly in the URL fragment. Vulnerable to token Interception via URL history, referrer headers, and browser extensions.
Token Security
Section titled “Token Security”Storage
Section titled “Storage”| Location | Vulnerable to XSS? | Vulnerable to CSRF? | Recommendation |
|---|---|---|---|
localStorage | Yes | No | Never use for sensitive tokens |
sessionStorage | Yes | No | Never use for sensitive tokens |
httpOnly cookie | No | Yes (mitigated by SameSite) | Recommended for web apps |
| In-memory (SPA) | No (until refresh) | N/A | Good for SPAs with silent refresh |
// Recommended: httpOnly cookie for tokens (set by server)// Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
// SPA: store access token in memory, refresh token in httpOnly cookielet accessToken = null;
// On page load, use refresh token to get a new access tokenasync function initializeAuth() { const response = await fetch('/api/auth/refresh', { method: "POST'', credentials: "include', // sends httpOnly cookie }); const data = await response.json(); accessToken = data.access_token;}
// Access token is lost on page refresh (acceptable: silent refresh restores it)Token Binding
Section titled “Token Binding”DPoP (Demonstrating Proof-of-Possession) binds a token to a specific client:
// Create a DPoP proof for each requestasync function createDPoPProof(url, method) { const jwk = await crypto.subtle.generateKey({ name: "ECDSA'', namedCurve: "P-256' }, true, [ 'sign', ]);
const header = { typ: "dpop+jwt'', jwk: exportJWK(jwk) }; const payload = { jti: crypto.randomUUID(), htu: url, htm: method, iat: Math.floor(Date.now() / 1000), };
const signed = await signJWT(header, payload, jwk); return signed;}
// Include DPoP header in every requestfetch("/api/resource', { headers: { Authorization: "Bearer xxx'', DPoP: dpopProof, },});Audience and Issuer Validation
Section titled “Audience and Issuer Validation”# ALWAYS validate aud and iss on every tokendef validate_token(token): claims = decode_jwt(token)
# Validate issuer if claims["iss'] != 'https://auth.example.com': raise InvalidTokenError(f"Wrong issuer: {claims['iss']}")
# Validate audience (token must be intended for your service) if 'my-api-id' not in claims.get('aud', []): raise InvalidTokenError(f"Wrong audience: {claims.get('aud')}")
return claimsCommon OAuth Vulnerabilities
Section titled “Common OAuth Vulnerabilities”Redirect URI Manipulation
Section titled “Redirect URI Manipulation”// VULNERABLE: Accept any redirect URIapp.get('/callback', (req, res) => { const redirectUri = req.query.redirect_uri; // If attacker provides redirect_uri=https://evil.com, the auth code goes to evil.com});
// SAFE: Whitelist redirect URIsconst ALLOWED_REDIRECTS = ['https://myapp.com/callback', 'https://admin.myapp.com/callback'];
app.get('/callback', (req, res) => { const redirectUri = req.query.redirect_uri; if (!ALLOWED_REDIRECTS.includes(redirectUri)) { return res.status(400).send('Invalid redirect URI'); }});Token Leakage via URL
Section titled “Token Leakage via URL”Authorization codes in the URL can leak via:
- Browser history
- Referrer headers (if the page links to an external site)
- Server access logs
- Browser extensions
The authorization code flow mitigates this because the code is exchanged server-side and is Single-use. But if the code is intercepted, PKCE prevents the attacker from exchanging it.
CSRF via State Parameter
Section titled “CSRF via State Parameter”The state parameter prevents CSRF attacks on the OAuth flow:
// Step 1: Generate state before redirecting to auth serverconst state = crypto.randomUUID();session.oauthState = state; // Store in server-side session
// Step 2: Include state in authorization requestconst authUrl = `https://auth.example.com/authorize?state=${state}&...`;
// Step 3: On callback, verify state matchesapp.get('/callback', (req, res) => { if (req.query.state !== session.oauthState) { return res.status(400).send('CSRF detected: state mismatch'); } session.oauthState = null; // Consume the state // Proceed with token exchange});PKCE Bypass
Section titled “PKCE Bypass”PKCE can be bypassed if:
- The
code_challenge_methodis set toplain(verifier sent in cleartext) - The authorization server does not validate the
code_challengeagainst thecode_verifier
Always use S256 (SHA-256) as the challenge method.
OAuth 2.1 Changes
Section titled “OAuth 2.1 Changes”OAuth 2.1 (draft) consolidates best practices from OAuth 2.0 security BCPs:
| Change | Impact |
|---|---|
| Implicit grant removed | All clients must use authorization code + PKCE |
| Password grant removed | No more credential sharing with clients |
| PKCE required for all clients | Even confidential clients must use PKCE |
| Redirect URI must use HTTPS | HTTP redirect URIs no longer allowed |
| Exact redirect URI matching | No partial/path-based matching |
| Sender-constrained tokens | DPoP or mTLS for proof-of-possession |
Common Pitfalls
Section titled “Common Pitfalls”Using Access Tokens as Identity
Section titled “Using Access Tokens as Identity”Access tokens are for authorization, not authentication. The sub claim may be absent. Use the id_token (OIDC) for identity information. Never use an access token to make authentication Decisions.
Not Validating Tokens on Every Request
Section titled “Not Validating Tokens on Every Request”Token validation must happen on every API request. Caching validation results is acceptable if you Check the exp claim first. Never trust a token without verifying its signature, issuer, audience, And expiration.
Storing Refresh Tokens Without Rotation
Section titled “Storing Refresh Tokens Without Rotation”Without refresh token rotation, a stolen refresh token can be used indefinitely. Implement rotation: Issue a new refresh token on every refresh, and revoke all tokens for the user if a rotated token is Reused.
Missing Scope Enforcement
Section titled “Missing Scope Enforcement”Access tokens contain scope claims. The resource server must check that the token has the required Scope for the requested operation:
def require_scope(required_scope): def decorator(f): def wrapper(*args, **kwargs): token = get_bearer_token() claims = validate_token(token) if required_scope not in claims.get('scope', '').split(): raise ForbiddenError("Insufficient scope") return f(*args, **kwargs) return wrapper return decorator
@app.route('/api/admin/users')@require_scope('admin:users')def list_users(): ...Accepting Tokens from Untrusted Issuers
Section titled “Accepting Tokens from Untrusted Issuers”Always validate the iss claim against a known list of trusted issuers. If your API accepts tokens From https://auth.example.com but also accepts tokens from any issuer, an attacker can create Their own authorization server and issue tokens with arbitrary claims.
Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to oauth 2.0 deep dive, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.