OAuth Flows Compared — Code, PKCE, Implicit, Device, OIDC
OAuth Flows Compared
Section titled “OAuth Flows Compared”First, fix the category error
Section titled “First, fix the category error”These are not five peers:
Authorization Code ─┐Authorization Code + PKCE ─┴─ same flow. PKCE hardens it. Not a separate flow.Implicit ─── its own flow, deprecatedDevice Code ─── its own flow, nicheOpenID Connect (OIDC) ─── NOT a flow. A LAYER riding on the code flow.Three flows, one hardening, one layer.
The comparison
Section titled “The comparison”| Flow | Client type | client_secret |
redirect_uri |
How the token arrives | Status |
|---|---|---|---|---|---|
| Authorization Code | confidential (backend) | ✓ required | ✓ required | back-channel POST | recommended |
| Code + PKCE | public (SPA/mobile/CLI) | ✗ — verifier instead | ✓ required | back-channel POST | recommended for all |
| Implicit | SPA (legacy) | ✗ | ✓ required | URL fragment, front channel | deprecated |
| Device Code | TV, IoT, CLI, headless | ✗ | ✗ none | polling → back channel | niche, opt-in |
| OIDC | any (on top of code flow) | depends | ✓ required | id_token via the code flow |
the identity standard |
How they differ in the request
Section titled “How they differ in the request”Auth Code response_type=codePKCE response_type=code + code_challenge + code_challenge_method=S256Implicit response_type=token (or id_token for OIDC implicit)Device no /authorize redirect at all → POST /device/code, then poll /tokenOIDC scope=openid → response gains an id_tokenAuthorization Code
Section titled “Authorization Code”1. 302 → /authorize?response_type=code&client_id&redirect_uri&scope&state2. user consents3. 302 → /callback?code=..&state=..4. back channel: POST /token grant_type=authorization_code + code + client_secret + redirect_uri5. → access_token (+ refresh_token if offline_access)The code travels the front channel (browser URL); the token is fetched over the back channel. Full detail in the OAuth page.
PKCE — hardening, not a new flow
Section titled “PKCE — hardening, not a new flow”Same shape as above, plus a per-flow proof so a stolen code is useless:
1. client generates code_verifier = random 43-128 chars (kept local) code_challenge = base64url(SHA256(verifier))2. /authorize?...&code_challenge=..&code_challenge_method=S2563. /callback?code=..4. POST /token + code + code_verifier ← server recomputes the hash and comparesWhy it exists: the code travels the browser and must be assumed leakable. PKCE binds the exchange to whoever started the flow — no pre-shared secret needed, so it works for public clients.
The downgrade attack: if the server doesn’t require code_challenge, an attacker strips PKCE from step 2 and it protects nothing. Servers must enforce it for public clients.
Only S256 counts. plain provides no protection.
OAuth 2.1 requires PKCE for every authorization-code request.
Implicit — deprecated
Section titled “Implicit — deprecated”302 → app/#access_token=xyz&expires_in=3600 ↑ token in the URL FRAGMENT, no exchange stepWhy it died:
| Problem | Consequence |
|---|---|
| Token in the URL fragment | lands in browser history; readable by any JS on the page |
| No exchange step | no PKCE possible, no client authentication |
| Historically no refresh token | user re-authenticates every expiry |
| Any XSS | instant token theft |
The one honest advantage: a fragment is never sent to a server, so it stays out of server logs. Not enough to save it.
Replaced by Code + PKCE — same public-client capability, but through a single-use, verifier-bound exchange.
Device Code
Section titled “Device Code”For devices with no browser or no keyboard:
1. device → POST /device/code (client_id only) ← { device_code, user_code: "ABCD-1234", verification_uri: "github.com/login/device", interval: 5 }2. display to the user: "go to github.com/login/device and enter ABCD-1234"3. user opens it on their phone/laptop and approves4. device → POST /token grant_type=urn:ietf:params:oauth:grant-type:device_code ← authorization_pending ...retry every `interval` seconds ← { access_token } once approvedSame /token endpoint as always. No redirect_uri, no client_secret — the device can’t receive a redirect, so it polls instead.
Real users: gh auth login, docker login on a headless box, smart TVs, printers, Raspberry Pi.
Security warning — why it’s opt-in per app (GitHub’s own guidance):
The device flow does not require redirect URIs at all, which means that an attacker can use the device flow to remotely impersonate your app as part of a phishing attack.
With no redirect_uri to constrain the flow, an attacker holding your client_id can drive a device flow and show a victim GitHub’s real consent screen branded with your app’s name. Enable it only for genuinely constrained environments.
Not used by: authorization-code clients, and it has no PKCE (nothing to intercept).
OIDC — the layer, not a flow
Section titled “OIDC — the layer, not a flow”OAuth 2.0 → "this app may access these resources" (authorization)OIDC → "and the user is <identity>" (authentication)Rides the authorization code flow and adds:
| Addition | Purpose |
|---|---|
scope=openid |
turns on OIDC |
id_token |
a signed JWT with sub, email, iss, aud, exp |
/userinfo |
endpoint for claims |
nonce |
anti-replay for the id_token — distinct from state |
/.well-known/openid-configuration |
discovery: endpoints + public keys |
| JWKS | public keys to verify the id_token signature offline |
Plain OAuth never tells you who the user is. It was designed for delegated access, not identity. “Sign in with Google” is OIDC.
OIDC is a spec, not a platform
Section titled “OIDC is a spec, not a platform”OIDC = a protocol (OpenID Connect 1.0, OpenID Foundation)Google / Okta = identity providers (IdP) that implement ityour app = the relying party (RP)Three things named “OpenID”:
| Name | What | Status |
|---|---|---|
| OpenID 2.0 | old decentralized identity (identity = a URL) | dead |
| OpenID Connect (OIDC) | OAuth 2.0 + identity layer | current |
| OpenID Foundation | the org stewarding the spec | an org |
GitHub vs Google — the practical difference
Section titled “GitHub vs Google — the practical difference”GitHub is an OAuth provider, not an OIDC IdP for user login. Its token response has no id_token:
{ "access_token": "...", "token_type": "bearer", "scope": "...", "expires_in": 28800 }So identity requires a second call: GET /api.github.com/user with the bearer token. That’s OAuth doing an identity job — trustworthy only because the call goes to GitHub’s own API.
Google is a true OIDC provider. With scope=openid the token response includes a signed id_token, verifiable offline against Google’s JWKS — no extra call to learn who logged in.
Google → id_token ✓ (true OIDC) Okta, Auth0, Keycloak, Entra, Apple → id_token ✓GitHub → no id_token (OAuth + /user call)(The id_token claim on an unvalidated JWT is the whole reason the JWT page matters — alg:none, weak HMAC secret, unchecked aud/iss/exp.)
Attack surface per flow
Section titled “Attack surface per flow”| Flow | Where it breaks |
|---|---|
| Auth Code | redirect_uri manipulation, code interception, missing/ignored state |
| Code + PKCE | nothing new — but downgrade if the server doesn’t require code_challenge; plain method accepted |
| Implicit | token in URL fragment → history, XSS reads it, no refresh token |
| Device Code | phishing — no redirect_uri constrains the flow; attacker impersonates your app |
| OIDC | nonce replay; id_token is a JWT → alg:none, weak secret, unvalidated aud/iss/exp |
Practical rule
Section titled “Practical rule”New app, any client type → Authorization Code + PKCEBackend-only service → Code + PKCE (or client_credentials when no user)CLI / IoT / TV → Device Code (only if genuinely needed)Sign-in (identity) → OIDC on top of Code + PKCEImplicit → never. Legacy only.Sources
Section titled “Sources”- RFC 6749 (OAuth 2.0), RFC 7636 (PKCE), RFC 8628 (Device Authorization Grant), RFC 8725 (JWT BCP)
- OpenID Connect Core 1.0
- OAuth 2.1 draft — PKCE required, implicit and password grants removed
- GitHub docs: authorizing OAuth apps; device flow guidance; PKCE changelog (2025-07-14)
- OAuth.com Playground — observed flows