Security
Part 1 of 7 · OAuth & OIDCOAuth 2.1 & OIDC — Authorization Code + PKCE
OAuth 2.1 makes Authorization Code + PKCE the default for public clients and retires implicit and password grants. Authentication (who) and authorization (what) are different questions: OIDC identity is the next lesson, JWT is only a token format, and the decision matrix picks session, BFF, PKCE, client credentials, device code, or mTLS. Interviews expect the redirect sequence, S256, exact redirect URIs, and state versus nonce.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Why Auth Code + PKCE is the interview default
Prefer
Authorization Code + PKCE (S256)
The browser only ever sees a short-lived code. The token endpoint requires the code_verifier that started the flow. Public clients do not embed a secret.
- Required for public clients in OAuth 2.1.
- Confidential clients still use a secret — and should add PKCE anyway.
- Pairs with exact redirect-URI allowlists and opaque state.
- OIDC adds openid scope, id_token, and nonce on the same dance.
Alternative
Implicit, ROPC, or a secret in the SPA
These used to ship. OAuth 2.1 retired the first two. The third is just a public client in denial.
- Implicit puts tokens in the front channel (history, Referer, XSS).
- Password grant is a phishing-shaped API and breaks MFA / federation.
- A client secret in JavaScript is extractable — treat the app as public + PKCE.
- Machine-to-machine is Client Credentials, not a user redirect. See S2S.
Happy path — Authorization Code + PKCE
Vertical cards for phones. Sequence diagram below is the same pipeline.
- 1
Create verifier + S256 challenge
High-entropy code_verifier. Challenge is BASE64URL(SHA-256(verifier)). Store the verifier with the login session. - 2
Redirect to /authorize
response_type=code, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method=S256. OIDC adds openid and nonce. - 3
User authenticates and consents
Happens at the authorization server. The client never sees the password. - 4
Callback with code + state
Verify state against the session before doing anything else. Mismatch → fail closed. - 5
POST /token with the verifier
grant_type=authorization_code, code, redirect_uri, code_verifier. AS checks challenge↔verifier, then returns tokens.
Overview
OAuth 2.1 collapses a decade of BCPs into one profile: public clients must use Authorization Code + PKCE; implicit and resource-owner password grants are gone. OIDC layers identity (id_token, UserInfo, nonce) on top of OAuth's delegated authorization.
Senior interviews expect you to draw the redirect dance, explain why PKCE beats a client secret on SPAs and mobile, and name what breaks if you skip state / nonce. Token shape, refresh theft, browser storage, and S2S are sibling pages — do not fold them into this whiteboard.
You should be able to:
- Draw public vs confidential clients and say which proof each presents at
/token. - Walk code interception with and without PKCE.
- Separate OAuth (can this client call that API?) from OIDC (who is the user?).
AuthN vs AuthZ (do not conflate)
| Concern | Question | Typical artifact |
|---|---|---|
| Authentication (AuthN) | Who are you? | ID token, session cookie, mTLS cert, SPIFFE ID |
| Authorization (AuthZ) | What may you do? | Access token scopes, policy (OPA/Cedar), ACLs |
OAuth 2 issues access tokens so a resource server can authorize a call. OIDC adds ID tokens and UserInfo so the client can authenticate the end user. A JWT can carry either — or both — but the protocol decides the meaning, not the encoding. Stuffing an ACL into a JWT does not make the JWT the policy engine.
OAuth roles
| Role | Responsibility | Examples |
|---|---|---|
| Resource owner | Owns the data and grants consent | End user |
| Client | App requesting access | SPA, mobile app, backend |
| Authorization server | Authenticates the user or client and issues tokens | Auth0, Okta, Keycloak, Cognito |
| Resource server | Hosts the API and validates the access token | Your services |
Confidential clients can hold a secret or private key (BFF, traditional server app). Public clients cannot (SPA, native) — PKCE, never an embedded secret.
JWT is a format, not a protocol
A compact JWT is three base64url segments: header, payload, signature. Decoding them does not tell you how the user logged in, how refresh works, or which API the token is for. Those rules come from OAuth, OIDC, session design, or service identity. JWS signs a readable payload. JWE encrypts. Most access and ID tokens are JWS.
Full anatomy, alg=none, and kid injection: JWT vs opaque tokens.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Decision matrix
| Scenario | Prefer |
|---|---|
| Browser app, same-site API | Session cookie (HttpOnly, Secure, SameSite) or a BFF |
| SPA calling many APIs, or mobile | Auth Code + PKCE, short access JWT, rotating refresh |
| Machine-to-machine, no user | Client Credentials (confidential) or mTLS / SPIFFE |
| TV, CLI, input-constrained device | Device Code |
| Mesh or Kubernetes workload | SPIFFE/SPIRE or cloud workload identity, optional token exchange |
| User identity at the client | OIDC ID token plus nonce |
| Fine-grained AuthZ | Scopes plus policy on the resource server — do not overload the JWT with PII |
| Approach | Identity proof | Revocation | Typical use |
|---|---|---|---|
| OAuth + OIDC | IdP login + tokens | Refresh revoke or introspection | User-facing APIs |
| API keys | Shared secret | Delete the key | Simple legacy M2M |
| mTLS | Cert private key | Short cert TTL | Zero-trust S2S |
| Signed cookie session | Server secret | Delete the session row | Monolith or BFF |
API keys everywhere skip delegated consent, discovery, and a standard third-party client story.
Grants and client types
| Approach | Who proves what | Browser sees tokens? | OAuth 2.1 status | Best when |
|---|---|---|---|---|
| Auth Code + PKCE (public) | Code + code_verifier | No (code only on redirect) | Required for public | SPA, native, CLI with loopback |
| Auth Code + secret (confidential) | Code + client secret (+ PKCE recommended) | No | Allowed | Server-side web / BFF |
| Implicit | Nothing at token endpoint | Yes (fragment) | Removed | Never (legacy) |
| Resource Owner Password | User password to client | N/A | Removed | Never |
| Client Credentials | Client secret / assertion | N/A | OK (S2S) | Machine-to-machine — S2S study |
| Device Code | User on a second device | No | OK | TVs, CLIs without a browser |
What fails if you choose wrong
- Implicit / tokens in the URL → XSS, Referer leakage, browser history.
- Password grant → phishing-shaped API; breaks MFA and federation.
- Confidential secret embedded in a SPA → extractable; treat as public + PKCE.
- Skipping PKCE on a public client → a stolen authorization code is enough.
- Prefix
redirect_urimatching → open-redirect tricks steal the code. Require an exact allowlist match. code_challenge_method=plain→ the challenge is the verifier. Require S256.- SPAs using Client Credentials → the secret ships in JavaScript. M2M stays server-side.
Device Code and confidential client auth
Device Code (RFC 8628) is for a TV or CLI that cannot host the browser. The device posts to the device-authorization endpoint, shows a user_code and verification URL, and polls /token with grant_type=urn:ietf:params:oauth:grant-type:device_code. The user approves on a second device. Keep the user code short-lived, branded, and rate-limited so it is not a phishing coupon. Refresh tokens show up here and on Auth Code (often with offline access). Client Credentials usually does not return a refresh token.
Confidential clients should still send PKCE (OAuth 2.1 defense in depth). Prefer private_key_jwt or mTLS client authentication over a long-lived shared client_secret. A password grant is almost never acceptable: it is phishable and it skips the IdP's MFA redirect. Migrate it to Auth Code or Device Code.
Auth Code fail-closed checks
Any miss is invalid_grant or invalid_request. Do not continue.
- 1
state matches the session
Mismatch is login CSRF. Drop the callback. - 2
redirect_uri is an exact allowlist hit
Sibling paths, extra query, and open redirects must fail. The token request repeats the same URI. - 3
PKCE S256 verifier matches
A stolen code without the verifier is code interception. - 4
Confidential client authenticates
Secret, private_key_jwt, or mTLS. Public clients skip the secret and still send the verifier. - 5
Code is one-time and short-lived
Second redemption is replay.
OAuth vs OIDC
- OAuth 2.x: delegated authorization — can this client call that API as the user?
- OIDC: authentication plus identity claims — who is the user? — via
id_token/ UserInfo on top of OAuth.
Same redirect. Different question. Mixing them up is the fastest way to fail the follow-up.
Authorization Code + PKCE — step by step
- Client creates a random
code_verifier, derivescode_challenge=BASE64URL(SHA256(verifier)), methodS256. - Redirect the user to the AS
/authorizewithresponse_type=code,client_id,redirect_uri,scope,state,code_challenge,code_challenge_method=S256. For OIDC,scopeincludesopenidand you sendnonce. - User authenticates and consents at the AS.
- AS redirects to
redirect_uri?code=...&state=.... - Client verifies
state, then POSTs to/tokenwithgrant_type=authorization_code,code,redirect_uri,code_verifier(plus secret if confidential). - AS verifies challenge↔verifier and returns
access_token(plusrefresh_token, and for OIDCid_token).
Sequence
- 1
User → Client
Step 1 Click login
- 2
Client → Auth Server
Step 2 /authorize with code_challenge S256, state, nonce
- 3
Auth Server → User
Step 3 Login and consent
- 4
Auth Server → Client
Step 4 Redirect to the exact redirect_uri with code and state
- 5
Client → Client
Step 5 Check state matches this session
- 6
Client → Auth Server
Step 6 /token with code and code_verifier
- 7
Auth Server → Client
Step 7 access_token, refresh_token, id_token
- 8
Client → API
Step 8 Authorization Bearer access_token
- 9
API → API
Step 9 Verify signature, iss, aud, exp, scope
- 10
API → Client
200, or 401 and 403
- 11
Auth Server → Client
Failure path - invalid_grant, no tokens
Lesson map
OAuth 2.1 & OIDC — Authorization Code + PKCE
OAuth 2.1 makes Authorization Code + PKCE the default for public clients and retires implicit and password grants. Authentication (who) and authorization (what) are different questions: OIDC identity is the next lesson, JWT is only a token format, and the decision matrix picks session, BFF, PKCE, client credentials, device code, or mTLS. Interviews expect the redirect sequence, S256, exact redirect URIs, and state versus nonce.
Architecture. User Ready. Client Ready. Auth Server Ready. API Ready. Step 5 Check state matches this session Ready. Step 9 Verify signature, iss, aud, exp, scope Ready. Failure path - invalid_grant, no tokens Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB u["User Ready"] c["Client Ready"] as["Auth Server Ready"] rs["API Ready"] step_1["Step 5 Check state matches this session Ready"] step_2["Step 9 Verify signature, iss, aud, exp, scope Ready"] fail["Failure path - invalid_grant, no tokens Ready"] u -->|Step 1 Click login| c c -->|Step 2 /authorize with code_challenge S256, state, nonce| as as -->|Step 3 Login and consent| u as -->|Step 4 Redirect to the exact redirect_uri with code and state| c c -->|Step 5 Check state matches this session| step_1 c -->|Step 6 /token with code and code_verifier| as as -->|Step 7 access_token, refresh_token, id_token| c c -->|Step 8 Authorization Bearer access_token| rs rs -->|Step 9 Verify signature, iss, aud, exp, scope| step_2 rs -->|200, or 401 and 403| c as -->|Failure path - invalid_grant, no tokens| fail
PKCE deep dive
- Why: Authorization codes leak via custom URL schemes, malicious apps on mobile, or intermediary logs. PKCE binds the token exchange to the party that started the flow.
- S256 vs plain: Always
S256.plainexists only for constrained devices and is discouraged — the challenge is then the verifier, so stealing the authorize URL steals the proof. - Confidential clients: OAuth 2.1 still recommends PKCE even with secrets (defense in depth against code theft). PKCE does not replace the secret for a confidential client.
OIDC, one screen — full lesson is next
OIDC is OAuth plus identity. Scope openid asks for an ID token aimed at this client. The access token is what you send to APIs. Never present the ID token as an API bearer. Discovery, UserInfo, nonce, azp, and at_hash are the next page: ID tokens, UserInfo, discovery, and nonce.
state and nonce are orthogonal. state binds the browser redirect (CSRF). nonce binds the ID token to this authentication request. See the threat model.
Redirect URI rules (production pain)
- Exact match (or the AS-documented allowlist). No wildcards on most IdPs.
- Prefer
httpsapp callbacks, or private-use URI schemes / claimed HTTPS for native (RFC 8252). - Loopback
http://127.0.0.1:port/callbackis OK for native CLIs. - The
redirect_urion/tokenmust match the one used on/authorizeor you getinvalid_grant.
Sandbox: PKCE + fake authorize/token (Python)
Self-contained simulator — no network. A stolen code without the verifier must fail.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Same helpers (TypeScript)
WebCrypto PKCE pair plus an authorize URL builder. Storage of the verifier belongs in a BFF session, not localStorage.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Pitfalls
Draw a malicious Android app that registers the same custom scheme as yours. Show the victim completing login, the attacker receiving the code, and the /token call. Then add PKCE: which secret is missing on the attacker’s token request? Where did the honest app store code_verifier?
Interview Q&A
Why did OAuth 2.1 kill implicit?
Answer
Tokens in the front channel (URL fragment) leak via history, logs, Referer, and XSS. There is no client authentication at the token endpoint. Authorization Code + PKCE keeps tokens on the back channel.
Does PKCE replace client secrets?
Answer
For public clients, yes — the code_verifier is the proof at /token. Confidential clients still use a secret (or private-key JWT / mTLS) and should add PKCE so a stolen code is not enough.
What is the difference between OAuth and OIDC?
Answer
OAuth authorizes API access (access token at the resource server). OIDC authenticates the user to the client (ID token / UserInfo). JWT is neither protocol — it is a format. Depth: OIDC lesson.
Is a JWT authentication?
Answer
No. AuthN is the ID token or the session the server issued. AuthZ is access-token validation plus policy. Anyone can base64-decode a JWT; that peek is not a login.
Where does authorization live?
Answer
Coarse scopes on the token. Fine-grained decisions on the resource server or a policy engine. Do not stuff the entire ACL, or raw PII, into the JWT.
Walk me through code interception without PKCE.
Answer
A malicious app registers the same custom URL scheme, receives the authorization code, and exchanges it at /token — it gets tokens. With PKCE the attacker lacks code_verifier, so the AS returns invalid_grant.
Why S256, not plain?
Answer
The challenge is a hash. Stealing the authorize URL (and therefore the challenge) does not yield the verifier. plain puts the verifier in the authorize request, which defeats the point on anything that can leak query strings.
Where should SPAs keep tokens?
Answer
Prefer a BFF plus httpOnly cookies. If the browser holds a bearer token, treat XSS as full compromise for the token lifetime.
What goes wrong if state is omitted?
Answer
Login CSRF / session-swap: the attacker starts a login, the victim completes it, and the attacker’s code or session is bound to the victim’s browser (or vice versa). Depth: threat model.
Device Code vs Auth Code?
Answer
Device Code is for input-constrained devices (TV, CLI without a browser). The user authorizes on a second device with a user code. Auth Code + PKCE is the default when the client can open a browser.
Why must redirect_uri match on /token?
Answer
It stops an attacker who leaked a code from redeeming it at a different callback they control. Combined with an exact allowlist, this is the open-redirect control.
Client Credentials instead of Auth Code?
Answer
No user. The client is the subject. Use it for service-to-service calls, not for logging a human in.
Go Deeper
- OAuth 2.1 draft (IETF)
- RFC 7636 — PKCE
- OpenID Connect Core 1.0
- RFC 8252 — OAuth for Native Apps
- RFC 9700 — OAuth 2.0 Security BCP
- Auth0 — Authorization Code Flow with PKCE
- Okta — OAuth 2.0 and OIDC overview
- Aaron Parecki — OAuth 2.1: Key Updates for Developers
- RFC 6749 — OAuth 2.0
- RFC 8628 — Device Authorization
- oauth.net — Grant types
- Auth0 — Which OAuth 2.0 flow should I use?
- Next: OpenID Connect — ID tokens, UserInfo, discovery, nonce