From naked auth code · the interception attack · PKCE fix · OIDC · JWT inspector
1. The four actors click each
OAuth lets a user grant a third-party app limited access to their data on another site — without sharing their password. There are always four parties involved.
The problem OAuth solves: “I want to let this photo printing app access my Google Photos — but I don’t want to give them my Google password.” OAuth replaces password sharing with delegated access tokens that are scoped, time-limited, and revocable.
2. Authorization Code flow — without PKCE step through
The classic OAuth 2.0 flow as defined in RFC 6749 (2012). Click Next step to walk through it. The actor lighting up shows who is doing what.
Step 0 of 8
⚠ This flow has a critical weakness on mobile / SPA / native apps: the authorization code travels back through the user’s browser/OS as a redirect. If a malicious app on the same device can intercept that redirect, it can steal the code — and (depending on the client type) exchange it for tokens. We’ll demo that next.
3. The authorization code interception attack live demo
Mobile apps register custom URL schemes like myapp://callback. If a malicious app registers the same scheme, Android/iOS may dispatch the redirect to whichever app responds first. The attacker now has the authorization code.
📱 The setup
📱
Real App
😈
Malicious App
🏛️
Auth Server
Both apps installed on the same phone. Both registered myapp://callback in their manifest. The auth server has no idea which one is legitimate.
🚨 The attack runs
What about client_secret? Web servers protect the token exchange with a client_secret the attacker doesn’t have. But mobile and SPA apps cannot safely store secrets — they ship in the app binary or browser. So those clients are public clients with no secret. The attacker just needs the authorization code.
4. PKCE — Proof Key for Code Exchange RFC 7636
PKCE (pronounced “pixie”) replaces the static client_secret with a one-time secret that the legitimate client generates fresh for each authorization request. The attacker can’t steal it because it never travels over the wire in plaintext.
The math
Why this works
The legitimate app holds the only copy of code_verifier. It sends only the hash (code_challenge) to the authorization server in step 1. The verifier is sent in step 2 (token exchange) over a TLS-protected back-channel. An attacker who intercepts the authorization code has no way to compute the verifier from the hash — SHA-256 is one-way.
What changed: the “secret” that proves identity is no longer something the client must store securely. It’s generated fresh for each login, used once, and discarded.
5. Authorization Code + PKCE flow step through
Same flow as Section 2, but the highlighted lines show the PKCE additions. Notice: only two extra parameters (code_challenge and code_verifier) and one verification step on the server.
Step 0 of 10
6. Same attack against PKCE — what changes side by side
The malicious app does everything the same way. Click run and watch where it fails.
❌ Without PKCE
✅ With PKCE
7. OIDC layer — OAuth + identity OpenID Connect
OAuth 2.0 was designed for authorization (“app X may read user’s photos”), not authentication (“who is the user?”). OpenID Connect adds an ID Token (a JWT) on top of OAuth to answer that question.
📜 Access Token
Audience: the resource server (API)
Format: opaque string OR JWT (provider-specific)
Purpose: “bearer of this token may call this API”
Don’t: parse claims to identify the user. The audience isn’t you.
🪪 ID Token (OIDC only)
Audience: the client app (you)
Format: always a signed JWT
Purpose: “here is who the user is”
Verify: signature against JWKS, iss, aud, exp, nonce
Discovery — how the client finds the auth server’s endpoints
GEThttps://accounts.google.com/.well-known/openid-configuration// Returns one JSON document with everything the client needs:
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"code_challenge_methods_supported": ["S256"],
"id_token_signing_alg_values_supported": ["RS256"]
}
JWKS (jwks_uri) is the public-key endpoint. Your app fetches it once, caches the keys, and uses them to verify the ID Token signature. Keys rotate periodically — cache with a sensible TTL and refetch on signature failure.
8. JWT claim inspector decode & explore
A JWT is three base64url segments joined by dots: header.payload.signature. The header and payload are not encrypted — they’re just base64-encoded JSON. Anyone can read them. The signature is what makes the contents tamper-proof.
Sample ID Token (click any segment below to decode)
Verification checklist when your app receives an ID Token:
Signature valid against the public key from JWKS (matched by kid header)
iss matches your expected issuer URL
aud contains your client_id
exp is in the future, iat is reasonable (clock skew ±5 min)
nonce matches the value you sent in the authorize request
at_hash matches first half of SHA-256 of the access token (defends against token substitution)
Implicit flow (response_type=token) — deprecated. Tokens travelled in URL fragments and ended up in browser history / referer headers. Replaced by Auth Code + PKCE for SPAs.
Resource Owner Password Credentials (ROPC) — deprecated. Defeats the entire point of OAuth (no password sharing). Replaced by Auth Code for first-party apps.
client_secret in mobile apps — never safe. Apps are reverse-engineered. Use PKCE instead.
Spring Boot:spring-boot-starter-oauth2-client (handles PKCE automatically since 5.7)
Node.js:openid-client
When does PKCE NOT replace client_secret?
Confidential clients (server-side web apps where the secret stays on the server) should use both PKCE andclient_secret. Defense in depth. The current OAuth security BCP recommends PKCE for all clients, public or confidential.