OAuth 2.0 lets apps act on your behalf without sharing your password
OpenID Connect (OIDC) adds authentication — it tells the app who you are
Authorization Code flow with PKCE is the standard for mobile and SPAs
Token validation (issuer, audience, expiry, signature) prevents 90% of production attacks
Biggest mistake: storing access tokens in localStorage — one XSS and they're gone
DPoP binds tokens to the client, preventing replay across endpoints
✦ Definition~90s read
What is OAuth 2.0 and OpenID Connect?
OAuth 2.0 is a delegated authorization framework that lets an application (a client) obtain limited access to a user's resources hosted by another service (the resource server) without exposing the user's credentials. It solves the fundamental problem of granting third-party apps scoped access—think 'allow this app to read your Google Calendar but not delete events'—by issuing short-lived access tokens rather than sharing passwords.
★
Imagine you're staying at a hotel.
OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0 that adds authentication: it introduces an ID token (a signed JWT) that proves who the user is, not just what they can access. Together, they form the backbone of modern API security and single sign-on (SSO), used by every major platform from Google and Microsoft to GitHub and Auth0.
The Authorization Code Flow is the recommended OAuth 2.0 flow for server-side apps, but in SPAs (single-page applications) it requires the Proof Key for Code Exchange (PKCE) extension to prevent interception of the authorization code. The flow works like this: the SPA redirects the user to the authorization server, the user authenticates and consents, the server returns an authorization code to the SPA's redirect URI, the SPA exchanges that code (along with a code verifier) for an access token and optionally a refresh token, then uses the access token to call APIs.
OIDC adds the ID token in that same exchange, containing claims like sub, email, and name, which the client must validate—checking the signature, issuer, audience, and expiration—before trusting.
A critical production pitfall in SPAs is the leakage of refresh tokens into client-side logs, error messages, or browser storage. Unlike access tokens (short-lived, typically minutes), refresh tokens are long-lived credentials that can mint new access tokens indefinitely.
If an SPA logs the full token response for debugging—or if an error handler serializes the token object into a log entry sent to a third-party service like Sentry or Datadog—that refresh token is exposed. Attackers who harvest it from logs can silently maintain access long after the user logs out.
Mitigations include never logging token values in production, using structured logging with sensitive field redaction, and preferring the BFF (Backend for Frontend) pattern where tokens never touch the browser at all. Token Binding and DPoP (Demonstration of Proof of Possession) further harden this by cryptographically binding tokens to the client's TLS session or a key pair, making stolen tokens useless for replay.
Plain-English First
Imagine you're staying at a hotel. Instead of giving every restaurant and spa inside the hotel a copy of your passport, the front desk gives you a key card that says 'this guest can use the pool and restaurant, but not the business lounge.' OAuth 2.0 is that key card system — it lets a third-party app do specific things on your behalf without ever seeing your password. OpenID Connect is the hotel also printing your name and photo on the card so the spa knows who you are, not just what you're allowed to do.
In technical terms, OAuth uses tokens instead of passwords. The access token is like a limited-use key card that expires after a short time. The refresh token is like a voucher that lets you get a new key card without showing your passport again.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Every time a user clicks 'Sign in with Google' or grants a fitness app access to their calendar, OAuth 2.0 is running the show. It's the protocol that powers delegated authorization for billions of API calls every day — and it's also one of the most misunderstood protocols in production systems. Teams routinely ship broken or insecure OAuth implementations because they treat it like a black box, copy-paste an authorization URL, and call it done. That's a recipe for token leakage, privilege escalation, and account takeover at scale.
If you're building apps that integrate with third-party services or implementing your own auth server, understanding OAuth internals isn't optional. It's what separates a secure integration from a breach waiting to happen.
We'll skip the textbook definitions and get straight to what matters: the flows that work, the validation steps that matter, and the pitfalls that take down production systems. You'll see real code, real attacks, and the exact rules to prevent them. Here's the blunt truth: most OAuth failures are caused by things you can fix in five minutes — wrong endpoints, missing state, or skipping signature checks. Let's fix that now.
OAuth 2.0 & OpenID Connect — Delegated Authorization vs. Authentication
OAuth 2.0 is an authorization framework that lets a client application (e.g., a single-page app) obtain limited access to a resource server on behalf of a resource owner, without exposing the owner's credentials. It works by issuing an access token — typically a bearer token — that the client presents to the resource server. OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0 that adds authentication: it returns an ID token (a signed JWT) containing claims about the end-user, so the client can verify who the user is.
In practice, the flow starts with the client redirecting the user to the authorization server. After the user authenticates and consents, the server returns an authorization code to the client (via a redirect URI). The client exchanges that code — along with its client secret — for an access token, an ID token, and optionally a refresh token. The refresh token allows the client to get new access tokens without user interaction, but it must be stored securely. In a browser-based SPA, there is no safe place to store a refresh token — any JavaScript can read it, making it vulnerable to XSS attacks.
Use OAuth 2.0 when you need delegated access to APIs (e.g., "let this app post to my calendar"). Add OIDC when you also need to know who the user is (e.g., login with Google). For SPAs, the best practice is to use the Authorization Code Flow with PKCE (Proof Key for Code Exchange) and avoid refresh tokens entirely — instead, use short-lived access tokens and silent authentication via an iframe or the prompt=none parameter.
⚠ Refresh Tokens in SPAs Are a Security Risk
Storing a refresh token in browser storage (localStorage or sessionStorage) exposes it to any XSS vulnerability — treat it as a public credential and prefer short-lived tokens with PKCE.
📊 Production Insight
A payment SPA stored refresh tokens in localStorage. An XSS payload from a third-party analytics script exfiltrated the token, allowing attackers to mint new access tokens and call the payment API for 30 days until the refresh token rotated.
Symptom: Unauthorized transactions from legitimate user sessions, with no login anomalies — the attacker never needed the user's password.
Rule: Never issue refresh tokens to browser-based clients. Use the Authorization Code Flow with PKCE and short-lived access tokens (5–15 minutes), relying on silent re-authentication for session continuity.
🎯 Key Takeaway
OAuth 2.0 is for delegated access; OIDC adds authentication via ID tokens.
Refresh tokens in SPAs are a leak vector — use PKCE and short-lived tokens instead.
Always validate the ID token's signature, issuer, and audience on the client before trusting user identity.
The Authorization Code Flow — Step by Step
This is the most common OAuth 2.0 flow for server-side web apps. The user authorizes via the authorization server, receives a code, and the backend exchanges it for tokens. No tokens ever touch the browser — that's the whole point.
The flow: User clicks 'Login' → redirected to auth server → user authenticates and approves → auth server redirects back with a code → backend sends code + client credentials to token endpoint → receives access token (and optionally an ID token and refresh token).
PKCE (Proof Key for Code Exchange) adds a secret challenge to prevent code interception. No client secret needed — that's why it's mandatory for mobile apps and SPAs.
PKCE works by having the client create a cryptographically random string called the code_verifier, then sending its SHA-256 hash (code_challenge) during the authorization request. When exchanging the code for tokens, the client sends the original code_verifier, and the authorization server verifies that the hash matches. This prevents an attacker who intercepts the authorization code from exchanging it, because they don't have the code_verifier.
Additional security measures: - Always include a state parameter to prevent CSRF on the callback. - For OIDC, use the nonce parameter to bind the ID token to the session. - The redirect_uri must be an exact match — no wildcards. - The state value should be a cryptographically random string, stored server-side or in a session cookie for verification upon return.
A real-world nuance: In production, you'll see providers like Auth0 or Okta enforce PKCE even for confidential clients. Don't fight it — it's belt-and-suspenders security that costs nothing.
Timing warning: Authorization codes are extremely short-lived (often 1–5 minutes). If your token exchange fails because of a network timeout, you may need to restart the flow. Always have a retry strategy that redirects the user back to the authorize endpoint rather than retrying the same code.
Even if you have a client secret, always use PKCE for public clients (SPAs, mobile apps). The 'secret' in those environments is not really secret — it's embedded in code that anyone can read.
📊 Production Insight
The authorization code flow with PKCE prevents a class of attack called 'authorization code interception'.
Without PKCE, an attacker with access to the redirect (e.g., malicious browser extension) can steal the code and exchange it for tokens.
Rule: if your client is public (no ability to store a secret), PKCE is mandatory.
🎯 Key Takeaway
Authorization Code flow with PKCE is the gold standard for user-facing apps.
Tokens never reach the browser — only an ephemeral code.
If you're still using Implicit flow, migrate now.
Which OAuth flow should you use?
IfBackend web app (server-side rendering)
→
UseAuthorization Code flow (with client secret) — no PKCE needed unless you want extra security
IfSingle-page application (SPA) or mobile app
→
UseAuthorization Code flow with PKCE — do NOT use Implicit flow (deprecated)
IfMachine-to-machine (no user)
→
UseClient Credentials flow — the app authenticates as itself
IfLegacy system, no ability to store client secret
→
UseAuthorization Code flow with PKCE (still the only safe choice)
thecodeforge.io
Oauth2 Openid Connect
OpenID Connect — Adding Identity to OAuth
OAuth 2.0 handles authorization — what you can do. OpenID Connect (OIDC) handles authentication — who you are. OIDC extends OAuth with an ID token (a JWT) that contains the user's identity claims.
The ID token is signed by the authorization server using a private key. The client verifies the signature using the public key from the JWKS endpoint. The standard claims include sub (subject — unique user ID), iss (issuer), aud (audience — must match your client ID), exp, iat.
OIDC also defines the UserInfo endpoint — a protected API that returns the user's profile. You use the access token to call it. The UserInfo endpoint is useful for getting additional claims not included in the ID token, like profile picture or phone number. However, relying on UserInfo adds network latency; for performance-critical paths, include needed claims in the ID token.
OIDC scopes and claims: - openid scope is required to trigger OIDC - profile scope gives access to name, family_name, etc. - email scope returns email and email_verified - You can also request custom claims via the claims parameter (if the provider supports it) - The sub claim is a unique identifier for the user — never change it
Production gotcha: Some providers include the at_hash claim in the ID token. Always verify it if present — it ensures the ID token is bound to the access token. If you skip this, an attacker could swap tokens from another session.
Additional gotcha: The nonce claim in the ID token prevents replay of the ID token itself. If your provider supports it, use it. Without nonce, an attacker who intercepts an ID token could present it to your client and log in as that user, even if the session expired. The nonce ties the token to a specific authentication request.
io/thecodeforge/oauth/oidc-validate.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
# Validate an ID token using jq and openssl
ID_TOKEN="eyJ..." # your JWT
# Decode header and payload
HEADER=$(echo $ID_TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null)
PAYLOAD=$(echo $ID_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null)
echo "Header: $HEADER"
echo "Payload: $PAYLOAD"
# FetchJWKS and extract public key
JWKS=$(curl -s https://auth.example.com/.well-known/jwks.json)
# In production, use a library: jose, nimbus, etc.
# This script demonstrates the flow, not a secure implementation.
echo "JWKS keys available:"
echo "$JWKS" | jq -r '.keys[].kid'
You need both when your app needs to know the user AND act on their behalf.
Use OIDC for login; use OAuth scopes for API access.
📊 Production Insight
A common production mistake is treating the access token as proof of identity.
Access tokens are opaque to the client — they are meant for the resource server only.
Use the ID token for identity and the access token for API calls. Never expose the ID token to third parties.
🎯 Key Takeaway
OpenID Connect adds a signed ID token for authentication.
Always validate the ID token: check issuer, audience, expiry, signature, and nonce.
Access tokens are for APIs; ID tokens are for login.
thecodeforge.io
Oauth2 Openid Connect
Token Validation — Don't Trust, Verify
Tokens received from the authorization server must be validated before use. For JWTs, the validation steps are: 1. Check iss (issuer) matches your authorization server's identifier. 2. Check aud (audience) matches your client ID (for ID tokens) or the resource server ID (for access tokens). 3. Check exp is in the future. 4. Verify the signature using the public key from the JWKS endpoint.
For opaque tokens (like reference tokens), you must call the introspection endpoint. This adds latency but allows revocation.
Don't forget to check token_type — a misconfigured client might accept a 'Bearer' token that's actually a 'MAC' or 'DPoP' token.
Also important: check the nbf (not before) claim to reject tokens used before their valid time. And always allow for clock skew — typically 60 seconds — between your server and the authorization server.
Algorithm whitelisting critical: If you accept alg: none or alg: HS256 when you expect RS256, an attacker can forge tokens. Always whitelist the allowed algorithms in your JWT library.
JWKS caching: Fetch the JWKS endpoint and cache it with a reasonable TTL (e.g., 1 hour). But be ready to refetch on failure in case the provider rotated keys. Use the kid in the token header to select the correct key. Set up a background refresh job to keep the cache warm, or use a library that does it automatically. Otherwise, key rotation will break your token validation until the cache expires.
A real attack we've seen: A team validated signatures but forgot to check aud. An access token from Service A was used to call Service B because both services used the same JWKS endpoint. The fix: resource servers must check aud equals their own identifier. This is one of the most common OAuth misconfigurations we encounter.
Clock skew handling: Set your token validator to allow a tolerance of up to 2 minutes (120 seconds) to account for network delays and unsynchronized clocks. Too tight and you'll reject valid tokens; too loose and you open a window for replay.
If your server accepts tokens signed with alg 'none' or symmetric keys (HS256) when you expect RS256, attackers can forge tokens. Always whitelist the 'alg' parameter.
📊 Production Insight
Many breaches happen because token validation is skipped or improperly implemented.
For example, checking only the signature but not the audience allows token reuse across services.
Rule: validate issuer, audience, expiry, signature, algorithm, and nbf in every request.
🎯 Key Takeaway
Always validate tokens — don't just decode and trust.
Check issuer, audience, expiry, signature, algorithm, and nbf.
Local JWT validation is fast; introspection is secure. Choose based on your revocation needs.
JWT vs Opaque Token Validation
IfYou have the public key (JWKS) and low latency requirement
→
UseUse JWT — validate locally (no network call)
IfYou need instant revocation (e.g., user logout, permission change)
→
UseUse opaque tokens with introspection — the authorization server checks validity on each call
IfYou have a mix of services with different security needs
→
UseUse JWT for internal services (fast) and opaque tokens for edge APIs (revocable)
Common Production Pitfalls and How to Avoid Them
Even when you follow the flows correctly, production OAuth implementations fail silently. Here are the traps senior engineers see repeatedly:
Refresh token rotation without family tracking — if the same refresh token is used twice (e.g., two parallel requests), the second one invalidates the first. Your user gets logged out randomly. Solution: implement rotation families (the server keeps the previous refresh token valid for a short window).
Incorrect redirect URI validation — the authorization server must compare redirect URIs as exact strings (including trailing slash, query params). Some servers allow partial matching — that's a security hole. Always register the exact URI.
Storing tokens in localStorage — any XSS vulnerability leaks the token. Use secure, httpOnly cookies for web apps, or store tokens only in memory for SPAs and rely on short-lived sessions.
Ignoring the none signature algorithm — an attacker can modify the JWT header to alg: none and your server might accept it if validation is lenient. Always enforce a whitelist of accepted algorithms.
Mixing up authorization server endpoints — using the authorize endpoint for token exchange, or vice versa. Each endpoint has a strict purpose. Double-check your URLs.
Failing to handle clock skew — if your server's clock is more than 60 seconds off from the authorization server, valid tokens will be rejected. Use nbf and exp claims with a configured clock skew tolerance.
Missing state parameter — without state, the authorization callback is vulnerable to CSRF. An attacker can trick the user into exchanging an attacker's authorization code. Always generate a unique state value and verify it on return.
Not rotating signing keys — if the authorization server's private key is compromised, all tokens are forgeable. Implement key rotation and ensure your JWKS endpoint is updated.
Using the same client secret across environments — development, staging, and production should have different client credentials. A leak in a lower environment should not compromise production.
We've seen each of these cause real production outages. The first one (refresh token rotation without family) is especially insidious because it looks like a random logout to users. The second one (open redirect) is a compliance nightmare.
// WRONG — registering redirect_uri with wildcard
// "redirect_uris": ["https://*.myapp.com/callback"]
// CORRECT — exact match
"redirect_uris": ["https://app.myapp.com/callback"]
// Also wrong: not validating the 'aud' claim
// Correct validation includes audience check
// Additional pitfall: missing state parameter
// Worstcase: redirect_uri that allows open redirect
// Example of vulnerable registration:
// "redirect_uris": ["https://www.myapp.com/callback"]
// Attacker can exploit if server ignores query params
// Always validate exact match with no trailing slash
💡Security checklist for OAuth deployment
Use a tool like oauth.tools or a custom script that automates endpoint discovery, token validation, and common misconfiguration tests. Also: use DPoP (Demonstration of Proof of Possession) to bind tokens to a client key, preventing token replay across different clients.
📊 Production Insight
The most expensive OAuth bug I've seen was a misconfigured redirect URI that allowed open redirect to any domain.
An attacker crafted a link that looked like the real OAuth flow but redirected to evil.com after authorization.
Rule: validate redirect URIs exactly — no wildcards, no partial matches.
🎯 Key Takeaway
OAuth is simple in theory, fragile in practice.
Focus on redirect URI validation, token storage, and algorithm whitelisting.
Test your flow with an automated scanner before going to production.
Token Binding and DPoP — Protecting Against Token Replay
Standard OAuth 2.0 bearer tokens are like cash: whoever possesses them can use them. If a token is leaked, an attacker can replay it from any device. DPoP (Demonstration of Proof of Possession) binds a token to a specific client by requiring the client to prove possession of a private key.
The client generates a key pair and registers the public key with the authorization server during the token request. Every API call includes a DPoP proof JWT signed with the private key. The resource server verifies this proof before accepting the access token.
Benefits: - Token theft becomes useless: the token cannot be used without the private key. - Protects against token replay across endpoints (the proof includes the HTTP method and URI). - No need for client secrets — key pairs are ephemeral.
Trade-offs: - Requires cryptographic operations on every request (adds ~2ms per call). - More complex key management on the client side. - Not all providers support DPoP yet; check your authorization server. - DPoP tokens are not compatible with introspection endpoints that expect opaque bearer tokens.
Implementation steps: 1. Generate an EC or RSA key pair on the client. 2. Include the public key (thumbprint) as a cnf claim in the client's JWK. 3. When requesting a token, include the DPoP header with a signed proof. 4. For every protected request, include a fresh DPoP proof in the DPoP header.
When should you adopt DPoP? If you're handling financial transactions, healthcare data, or any scenario where token theft would be catastrophic. For most apps, short-lived tokens + refresh token rotation + strict CSP is sufficient, but DPoP is the next level of defense.
One more nuance: DPoP affects the token endpoint as well — the authorization server must verify the proof during token issuance. Some providers require a DPoP-bound token even for the initial authorization request. Check your provider's documentation.
DPoP is still new — many providers don't support it yet. But it's the future of token security. If you're building a high-security app, implement DPoP now. For most apps, short-lived tokens + refresh token rotation + strict CSP is sufficient.
📊 Production Insight
Bearer tokens are stolen via XSS, network interception, or browser history.
DPoP makes stolen tokens worthless because the attacker doesn't have the private key.
Rule: use DPoP for high-risk environments (financial, healthcare).
🎯 Key Takeaway
DPoP binds tokens to a specific client using a proof of possession.
This prevents token replay even if the token is stolen.
Adopt DPoP for environments where token theft is a realistic threat.
OAuth 2.0 Core Concepts — The Entities You'll Actually Deal With
Most explanations dump a diagram and call it a day. Let's talk about what each piece actually does when things break.
The Resource Owner is your user. They own the data. The Client is your application — the thing asking for access. These are not abstract roles. They are the systems you deploy and the people who use them.
The Authorization Server issues tokens after authentication. This is usually a separate service like Keycloak, Auth0, or a custom OIDC provider. Don't embed it in your monolith unless you enjoy security headaches.
The Resource Server hosts the actual data. Your API. It validates tokens before serving requests. Never trust the client to tell you who they are. The Resource Server must validate every single request.
Scopes define what the client can do — read, write, delete. Access Tokens are opaque strings or JWTs that carry this authorization. They expire. Refresh tokens exist to get new access tokens without re-authenticating the user.
You will debug scope mismatches and expired tokens in production. Understand these actors cold before writing a single line of auth code.
TokenValidationDebug.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// io.thecodeforge — system-design tutorial
import jwt
import requests
from datetime import datetime, timezone
# You stole this from a production incident where a stale token# caused a 5-hour outage because nobody checked expiry server-side.defvalidate_access_token(token: str, expected_audience: str, jwks_url: str) -> dict:
try:
# Fetch JWKS once and cache it. Please.
jwks_client = jwt.PyJWKClient(jwks_url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=expected_audience,
options={"verify_exp": True}
)
return payload
except jwt.ExpiredSignatureError:
print(f"[FATAL] Token expired at {datetime.now(timezone.utc)}")
raiseexcept jwt.InvalidAudienceError:
print("[FATAL] Token audience mismatch — are you using the wrong client?")
raiseexceptExceptionas e:
print(f"[FATAL] Token validation failed: {e}")
raise# Example usage — don't swallow exceptions in production.# validate_access_token("eyJ...", "api://production", "https://auth.example.com/.well-known/jwks.json")
Output
[FATAL] Token expired at 2025-04-08 14:23:19.042156+00:00
Traceback (most recent call last):
...
jwt.exceptions.ExpiredSignatureError: Signature has expired
⚠ Production Trap:
Never validate tokens client-side only. The Resource Server must re-validate every single request. Clients can lie. Trust nothing that comes over the wire without verification.
🎯 Key Takeaway
OAuth 2.0 has four actors: Resource Owner, Client, Authorization Server, Resource Server. Know them by heart. Each one has a distinct failure mode you will hit.
How OAuth 2.0 Works — The Wire Calls You Can't Ignore
Authorization Code flow exists because front-channel tokens are a security hole. The user clicks “Sign in with Google,” your back end receives the redirect with a temporary code, and then your server exchanges that code for an access token via a direct back-channel POST. That exchange is the only call whose response contains the token—no JavaScript ever sees it. The redirect URI must match exactly what you registered, or the authorization server rejects the request. If your token expires after 15 minutes and your refresh token after 30 days, any session beyond that boundary forces a full re-authorization. Every millisecond of token validation latency adds up: verify the token’s signature, audience (aud), issuer (iss), and expiration (exp) before accepting it. Never trust a token sent in a query string; it ends up in server logs and browser history. Use the Authorization: Bearer header instead.
Never expose client_secret in a public SPA. Use PKCE (Proof Key for Code Exchange) to prevent interception of the authorization code even if the redirect is sniffed.
🎯 Key Takeaway
The only safe OAuth flow is Authorization Code with PKCE; the token must never cross the front channel.
Disadvantages — The Pain OAuth 2.0 Doesn't Solve Out of the Box
OAuth 2.0 gives you a token, but zero guidance on what to put inside it or how to prove who the user is. That means every team must independently solve identity representation, leading to brittle client-side logic that breaks when a token format changes. The spec also omits any standardized encryption or signing mechanism for the authorization code itself, leaving implementations vulnerable to code interception unless explicit PKCE is added. Token revocation is another gap: there is no mandatory way to invalidate a refresh token beyond a server-side blacklist, which introduces stale-token attack windows. Finally, scoping is purely advisory — a client can request scope "admin" and a resource server has no standard way to negotiate reduced access; it must either trust blindly or build custom enforcement. These holes force production systems to layer on OpenID Connect for identity, JWT profiles for structure, and custom revocation logic, adding complexity that the base spec was supposed to avoid.
Client request with revoked token → 401: Token revoked
⚠ Production Trap:
Relying on stateless token validation alone is insufficient. Always pair JWT checks with a short-lived revocation list (e.g., Redis TTL) to close the window between token invalidation and expiry.
🎯 Key Takeaway
OAuth 2.0 is an authorization framework, not an identity or security protocol — you must fill every gap yourself.
● Production incidentPOST-MORTEMseverity: high
Leaked Refresh Token in SPA Logs
Symptom
Users reported unauthorized actions on their accounts hours after logout. Investigation showed active sessions persisted even after token revocation.
Assumption
The team assumed OAuth tokens were automatically invalidated on logout and that client-side logging posed no risk because tokens were 'short-lived'.
Root cause
The SPA logged the full token response (including the refresh token) to the console during development. The logging was left enabled in production. An attacker with XSS could exfiltrate the refresh token and use it to mint new access tokens indefinitely.
Fix
Remove all token logging from client code. Use short-lived access tokens (15 min) with refresh token rotation — rotating every refresh invalidates the previous refresh token. Apply strict CSP headers to block inline scripts.
Key lesson
Never log tokens anywhere, especially client-side.
Refresh token rotation is non-negotiable for SPAs.
Assume every client-side value is compromised — validate server-side.
Always validate redirect URIs server-side; never trust client-provided redirect parameters.
Production debug guideDiagnose token failures, redirect errors, and authorization denials fast.6 entries
Symptom · 01
Redirect URI mismatch error
→
Fix
Check the exact redirect URI registered in the authorization server VS the one the client sent. Must match character-for-character (no trailing slashes, query params allowed only if registered).
Symptom · 02
Access token rejected by resource server
→
Fix
Validate token signature using the JWKS endpoint. Use jwt.io or curl to decode. Check iss, aud, exp claims. The audience must match the resource server's identifier.
Symptom · 03
Authorization code expired or already used
→
Fix
Authorization codes are single-use and short-lived (typ. 10 min). Ensure the token endpoint is called immediately after receiving the code. If retry, use PKCE to prevent replay.
Symptom · 04
Invalid grant type during token exchange
→
Fix
Verify the grant_type parameter: use authorization_code for code flow, client_credentials for client credentials, refresh_token for refresh. Ensure the value is exactly as specified.
Symptom · 05
'Invalid scope' error
→
Fix
Check the scopes requested in the authorization request match the scopes registered for the client. Scopes are case-sensitive and must be space-separated. Some providers require certain scopes like 'openid' for OIDC.
Symptom · 06
ID token signature validation fails
→
Fix
Fetch the JWKS endpoint and verify the kid in the token header matches a key. Ensure clock skew is ≤60s. If the server uses symmetric keys (HS256) and you expect RS256, you may be hit by algorithm confusion.
★ 3-Minute OAuth Incident ResponseWhen OAuth breaks under pressure, run these checks in order.
User cannot authenticate / redirect loops−
Immediate action
Check authorization server status and network connectivity.
from fastapi import FastAPI, HTTPException, Depends, Request
Disadvantages
Key takeaways
1
You now understand what OAuth 2.0 and OpenID Connect is and why it exists
2
You've seen it working in a real runnable example
3
Practice daily
the forge only works when it's hot 🔥
4
Authorization Code flow with PKCE is the standard for all public clients - never use Implicit
5
Always validate tokens
issuer, audience, expiry, signature, and algorithm whitelist
6
Store tokens securely
memory for SPAs, httpOnly cookies for web apps, never localStorage
7
Refresh token rotation is non-negotiable for security
use rotation families with a grace window
8
DPoP binding prevents token replay across clients
adopt it for high-security environments
9
Always include and validate the state parameter to prevent CSRF
10
Separate client credentials per environment
dev leaks should not affect prod
Common mistakes to avoid
9 patterns
×
Storing tokens in localStorage
Symptom
After an XSS attack, attacker exfiltrates the access token and makes API calls on behalf of the user.
Fix
Store tokens in memory only (SPA) or use secure httpOnly cookies (web app). For mobile, use secure storage like Keychain/Keystore.
×
Not using PKCE for public clients
Symptom
An attacker with access to the browser can intercept the authorization code and exchange it for tokens because no code verifier is required.
Fix
Always use PKCE for any client that cannot keep a secret. Generate a code_verifier and code_challenge; send the challenge in the authorize request and the verifier in the token request.
×
Accepting 'alg: none' in JWT validation
Symptom
An attacker crafts a token with header {"alg":"none"} and arbitrary payload. If the server doesn't check the algorithm, it accepts the token as valid.
Fix
Whitelist allowed algorithms in your JWT validation library. Reject tokens with alg: none or alg: HS256 when you expect RS256.
×
Using the implicit flow for modern apps
Symptom
Access token is exposed in the URL fragment. It can be leaked through browser history, referer headers, or server logs.
Fix
Migrate to Authorization Code flow with PKCE. The implicit flow is deprecated and insecure for modern SPAs.
×
Ignoring token expiry and not handling refresh failures
Symptom
Users get logged out randomly. Refresh token rotation fails silently, and the user sees an error instead of re-authenticating gracefully.
Fix
Implement proper refresh token rotation with a family model (short grace period for old tokens). Handle 401 errors from refresh by redirecting to the authorization endpoint.
×
Failing to rotate refresh tokens
Symptom
A compromised refresh token can be used indefinitely to obtain new access tokens.
Fix
Enable refresh token rotation: each token exchange returns a new refresh token and invalidates the previous one. Use a grace window for concurrent requests.
×
Missing state parameter in authorization request
Symptom
The authorization callback is vulnerable to CSRF. An attacker can trick a user into exchanging an authorization code that the attacker obtained, leading to token theft.
Fix
Always include a cryptographically random state parameter in the authorization request and validate it when the redirect returns. Reject mismatches immediately.
×
Not validating the token_type claim
Symptom
The resource server accepts a token with token_type: Bearer but the client sent a DPoP token — or vice versa. The token passes signature validation but the expected binding is wrong, leading to potential replay or bypass.
Fix
Check the token_type claim in the token response and enforce expected type (Bearer or DPoP) in your resource server. Reject if type doesn't match the negotiated flow.
×
Using same client secret across environments
Symptom
If development credentials are leaked, the attacker can request tokens for production because the same client_id and secret work.
Fix
Register separate clients for each environment. Use a different client_id and secret for dev, staging, and prod.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
Explain the difference between OAuth 2.0 and OpenID Connect.
Q02SENIOR
What is PKCE and why is it needed?
Q03SENIOR
How do you validate an ID token in production?
Q04JUNIOR
What is the difference between access token and refresh token?
Q05SENIOR
Describe a real-world OAuth security incident you've seen or would preve...
Q06SENIOR
How would you design a token revocation mechanism for a distributed syst...
Q07SENIOR
What is the purpose of the `state` parameter in OAuth?
Q08SENIOR
Explain how DPoP prevents token replay attacks.
Q09SENIOR
What's the difference between an ID token and an access token at the JWT...
Q10SENIOR
How would you implement OAuth for a microservices architecture with inte...
Q01 of 10JUNIOR
Explain the difference between OAuth 2.0 and OpenID Connect.
ANSWER
OAuth 2.0 is an authorization framework — it allows a client to access resources on behalf of a user. OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0. OIDC adds an ID token (a JWT) that contains identity claims about the user. In practice, OAuth 2.0 answers 'what can the app do?' while OIDC answers 'who is the user?'
Q02 of 10SENIOR
What is PKCE and why is it needed?
ANSWER
PKCE (Proof Key for Code Exchange) is an extension to the Authorization Code flow that prevents authorization code interception attacks. The client generates a cryptographically random code_verifier and sends its SHA-256 hash (code_challenge) during the authorization request. When exchanging the code for tokens, the client sends the original code_verifier. The server verifies that the hash matches. This way, even if an attacker intercepts the authorization code, they cannot exchange it without the code_verifier. PKCE is mandatory for public clients (SPAs, mobile) that cannot store a client secret securely.
Q03 of 10SENIOR
How do you validate an ID token in production?
ANSWER
Step-by-step:
1. Verify the token is a JWT with a supported algorithm (e.g., RS256).
2. Check the iss (issuer) matches the authorization server's expected URI.
3. Check the aud (audience) contains your client ID.
4. Validate the exp (expiry) and iat (issued at) are reasonable.
5. Verify the signature using the public key from the authorization server's JWKS endpoint. The kid header indicates which key to use.
6. (Optional) Call the UserInfo endpoint with the access token to get additional claims.
Use a well-tested JWT library for signature verification; don't implement it from scratch.
Q04 of 10JUNIOR
What is the difference between access token and refresh token?
ANSWER
An access token is used to authenticate API calls. It is short-lived (often 15-60 minutes) to limit damage if leaked. A refresh token is a long-lived credential used to obtain new access tokens without re-authenticating the user. Refresh tokens should be stored securely and sent only to the token endpoint. With refresh token rotation, each refresh returns a new refresh token and invalidates the old one, preventing replay attacks.
Q05 of 10SENIOR
Describe a real-world OAuth security incident you've seen or would prevent.
ANSWER
In one incident, a company's SPA used the Implicit flow and stored the access token in localStorage. An XSS vulnerability in a third-party widget allowed an attacker to exfiltrate the token and make API calls. The fix involved migrating to Authorization Code with PKCE, storing the token in memory (not localStorage), and implementing Content Security Policy headers to block inline scripts. Also, they introduced short token lifetimes and refresh token rotation to limit the blast radius.
Q06 of 10SENIOR
How would you design a token revocation mechanism for a distributed system?
ANSWER
For a distributed system, you need a central revocation store that all resource servers can access. Options:
- Blacklist: maintain a distributed cache (e.g., Redis) of revoked token IDs (JTI). Each resource server checks the cache on every request. The authorization server adds the token to the blacklist on logout or revocation. This adds latency but ensures immediate revocation.
- Opaque tokens with introspection: the resource server calls the authorization server's introspection endpoint on each call. The AS checks a central DB for revocation status. Higher latency but simpler.
- Short-lived tokens + refresh token rotation: tokens expire quickly (5-15 min), and revocation is enforced at the token endpoint. The user cannot get new tokens after revocation because the refresh token is invalidated. No shared state needed, but there's a small window where a token could still be used.
Recommendation: use short-lived access tokens with refresh token rotation for most scenarios. If you need instant revocation, combine with a blacklist cache for the access token's lifetime.
Q07 of 10SENIOR
What is the purpose of the `state` parameter in OAuth?
ANSWER
The state parameter is used to prevent Cross-Site Request Forgery (CSRF) on the authorization callback. The client generates a unique, cryptographically random value and includes it in the authorization request. When the user is redirected back, the client checks that the state value matches what was sent. If it doesn't, the request is rejected. Additionally, state can carry application state (like the page the user was on before login) to restore it after redirect.
Q08 of 10SENIOR
Explain how DPoP prevents token replay attacks.
ANSWER
DPoP (Demonstration of Proof of Possession) binds an access token to a specific client by requiring the client to prove possession of a private key. The client generates a key pair and includes the public key thumbprint in the token request. Every API call includes a DPoP proof JWT signed with the private key, containing the HTTP method and URI. The resource server verifies this proof before accepting the token. Even if the token is stolen, it cannot be replayed from a different client because the attacker lacks the private key, and the proof is tied to a specific endpoint and method.
Q09 of 10SENIOR
What's the difference between an ID token and an access token at the JWT level?
ANSWER
An ID token is a JWT that must contain sub, iss, aud, exp, iat. Its primary purpose is identity — it tells the client who the user is. An access token can be opaque or JWT. If a JWT, it typically contains scopes, a resource-server-specific audience, and a subject. Access tokens are not meant to be read by the client; they are for the resource server. In OIDC flows, the ID token is signed and the access token is also signed (if JWT). The ID token often includes nonce and at_hash; access tokens usually have scope and may not have an aud that matches the client.
Q10 of 10SENIOR
How would you implement OAuth for a microservices architecture with internal service-to-service calls?
ANSWER
Use Client Credentials flow for machine-to-machine communication. Each service registers as a confidential client with its own client_id and secret. For user-invoked calls, use token exchange: the edge service (API Gateway) validates the user's access token and then uses the token exchange grant to obtain a new token scoped for the downstream service. Alternatively, use JWT access tokens and propagate them: each internal service validates the same token (checking audience and scope). Use a shared JWKS endpoint for validation. Ensure each service checks the intended audience (its own identifier) to prevent token reuse across services. For revocation, use short-lived tokens and rely on the token expiry.
01
Explain the difference between OAuth 2.0 and OpenID Connect.
JUNIOR
02
What is PKCE and why is it needed?
SENIOR
03
How do you validate an ID token in production?
SENIOR
04
What is the difference between access token and refresh token?
JUNIOR
05
Describe a real-world OAuth security incident you've seen or would prevent.
SENIOR
06
How would you design a token revocation mechanism for a distributed system?
SENIOR
07
What is the purpose of the `state` parameter in OAuth?
SENIOR
08
Explain how DPoP prevents token replay attacks.
SENIOR
09
What's the difference between an ID token and an access token at the JWT level?
SENIOR
10
How would you implement OAuth for a microservices architecture with internal service-to-service calls?
SENIOR
FAQ · 9 QUESTIONS
Frequently Asked Questions
01
What is OAuth 2.0 and OpenID Connect in simple terms?
OAuth 2.0 and OpenID Connect is a fundamental concept in System Design. Think of it as a tool — once you understand its purpose, you'll reach for it constantly.
Was this helpful?
02
Is OAuth 2.0 an authentication or authorization protocol?
OAuth 2.0 is strictly an authorization protocol — it grants access to resources. It does not authenticate the user. OpenID Connect adds authentication on top by introducing the ID token, which contains identity claims.
Was this helpful?
03
Can I use OAuth 2.0 without OpenID Connect?
Yes, if you only need authorization (e.g., allowing an app to post on your behalf). But if you need to know the user's identity (e.g., login), you need OpenID Connect. Many providers offer both: the /oauth/authorize endpoint with scope=openid triggers OIDC.
Was this helpful?
04
What is the difference between scope and claims in OIDC?
Scopes request specific sets of claims. For example, scope=openid profile email requests the sub, name, and email claims in the ID token. Claims are the actual key-value pairs returned. You can also request individual claims via the claims parameter (if supported).
Was this helpful?
05
How do I handle token revocation on user logout?
For access tokens, you can let them expire (if short-lived) or use a blacklist. For refresh tokens, call the authorization server's revocation endpoint (/oauth/revoke) with the token. The server will invalidate it. Some implementations also rotate refresh tokens on logout.
Was this helpful?
06
What is the recommended lifetime for access tokens?
Access tokens should be short-lived — typically 15 to 60 minutes. Shorter lifetimes reduce the blast radius of a leak but increase the frequency of refresh token usage. Use 15 minutes for high-security apps, 60 minutes for lower-risk scenarios. Refresh tokens can last days or months, but should be rotated on each use.
Was this helpful?
07
What is DPoP and when should I use it?
DPoP (Demonstration of Proof of Possession) is an extension that binds an access token to a specific client by requiring the client to prove possession of a private key. Use DPoP in high-security environments where token theft via XSS or network interception is a realistic threat. It adds about 2ms per request for cryptographic overhead.
Was this helpful?
08
What is the difference between hash (at_hash) in the ID token and why does it matter?
The at_hash claim is a hash of the access token. It binds the ID token to the access token. If you skip verifying at_hash, an attacker could swap the access token from one session with another, potentially obtaining a token with different scopes or permissions. Always verify at_hash if present.
Was this helpful?
09
How does refresh token rotation work and why is it important?
Refresh token rotation means every time the client exchanges a refresh token for a new access token, the authorization server also returns a new refresh token and invalidates the old one. This limits the window of exposure: if a refresh token is stolen, it can only be used once before being rotated. However, to handle concurrent requests, implement a grace window where the previous refresh token remains valid for a few seconds.