Protocol · Tokens · Delegated Access
OAuth2 hands out a keycard. OIDC prints a name badge.
OAuth 2.0 lets an app act on your behalf without ever seeing your password — that's authorization. OpenID Connect adds a thin identity layer on top so the app can also learn who you are. Click through everything below — no JavaScript, just CSS doing the work.
OAuth 2.0 — RFC 6749
"Can this app do X on my behalf?"
Delegated authorization. The output is an access token scoped to specific permissions (repo:read, calendar:write). It says nothing about identity by itself.
OpenID Connect — built on OAuth2
"Who is this person, exactly?"
Federated authentication. The output is an ID token — always a signed JWT — plus a standard /userinfo endpoint. This is what "Sign in with Google" actually returns.
Rule of thumb: if you only see an access token, you have OAuth2. If you also get an ID token, you have OIDC.
Pick a grant type
Five flows, five very different trust situations
Tap a tab. Same cast of characters — Resource Owner, Client, Authorization Server, Resource Server — completely different choreography.
SPAs, mobile apps, CLIs — anything that can't keep a secret
The public client can't safely hold a client_secret, so it proves it's the same app that started the flow using a one-time cryptographic pair instead. This is the default choice for nearly everything today.
- 1. App generates
code_verifier(random) +code_challenge= SHA256(verifier) - 2. Browser → AS:
/authorize?...&code_challenge=...&code_challenge_method=S256 - 3. User authenticates + consents at the AS
- 4. AS → app: one-time
authorization_codevia redirect - 5. App → AS token endpoint: code + original
code_verifier(no secret) - 6. AS recomputes the hash, matches it, returns tokens
Full interactive walkthrough of this exact flow is below ↓
Traditional server-rendered web apps
Same shape as PKCE, but the client is confidential — it runs on a server, holds a real client_secret, and exchanges the code for tokens over a back-channel the browser never touches.
- 1. Browser → AS:
/authorize?response_type=code&client_id=... - 2. User authenticates + consents
- 3. AS → browser → your server:
authorization_code - 4. Server → AS token endpoint: code +
client_id+client_secret - 5. AS → server: access_token (+ id_token if OIDC, + refresh_token)
PKCE is now recommended here too, even for confidential clients — it's cheap insurance.
Machine-to-machine, no user in sight
A backend service authenticating as itself — a cron job hitting an internal API, one microservice calling another. There's no Resource Owner and no redirect.
- 1. Service → AS token endpoint:
grant_type=client_credentials+ client_id + client_secret - 2. AS validates the client itself
- 3. AS → service: access_token scoped to that service's own permissions
No ID token is issued here — there's no human to authenticate, so plain OIDC doesn't apply.
Smart TVs, CLIs, anything without a decent browser or keyboard
The device shows a short code and a URL; the user finishes the login on their phone or laptop while the device polls in the background.
- 1. Device → AS: request a
device_code+user_code - 2. Device displays: "Go to example.com/activate and enter ABCD-1234"
- 3. User approves on a second screen
- 4. Device polls the token endpoint with
device_codeuntil approved - 5. AS → device: access_token once the user says yes
Legacy SPA flow — don't build new things on this
Returned the access token directly in the URL fragment with no code-exchange step. Formally deprecated by OAuth 2.1: tokens leak into browser history, referrer headers, and server logs, and there's no refresh token.
- 1. Browser → AS:
/authorize?response_type=token - 2. User authenticates
- 3. AS → browser:
#access_token=...straight in the URL fragment
Replaced by Auth Code + PKCE, which every modern SPA framework supports out of the box.
Walkthrough
Authorization Code + PKCE, one hop at a time
Step through it with the buttons below.
Step 1 of 6 · Before the redirect
The app mints a secret it never sends yet
Locally, in memory: code_verifier = a random 43–128 character string. Then it computes code_challenge = base64url(SHA256(code_verifier)). Only the challenge — the hash — is going anywhere near the network.
Step 2 of 6 · Redirect to the Authorization Server
Browser is sent to /authorize
GET /authorize?response_type=code&client_id=abc123&redirect_uri=https://app.example/cb&scope=openid%20profile&state=xyz&code_challenge=E9Melhoa...&code_challenge_method=S256
state defends against CSRF. scope=openid is what turns this into an OIDC request instead of plain OAuth2.
Step 3 of 6 · At the Authorization Server
User authenticates and consents
Password, passkey, MFA, or an existing SSO session — whatever the AS already trusts. The AS shows a consent screen listing the requested scopes. The app's own password field never appears; the app never sees the credential.
Step 4 of 6 · Redirect back
A one-time code lands in the redirect_uri
GET https://app.example/cb?code=SplxlOBe...&state=xyz
The app checks state matches what it sent. This code is short-lived (often ~60s) and single-use — worthless without the matching verifier.
Step 5 of 6 · Back-channel token exchange
App trades the code for tokens
POST /token
grant_type=authorization_code&code=SplxlOBe...&redirect_uri=https://app.example/cb&client_id=abc123&code_verifier=dBjftJeZ...
The AS hashes the verifier itself and compares it to the challenge from Step 2. Match → proceed. Mismatch → reject. An attacker who intercepted the code in Step 4 still can't complete this step without the verifier, which never left the original app.
Step 6 of 6 · Tokens issued
Done — three possible tokens come back
access_token — call the API. id_token — because scope included openid, a signed JWT describing who logged in. refresh_token — get new access tokens later without another redirect.
Scroll down to pull these three apart.
Three tokens, three jobs
Access token ≠ ID token ≠ refresh token
The single most common OAuth2/OIDC bug is treating these as interchangeable. They aren't.
For the Resource Server — "here's what this bearer may do"
Sent as Authorization: Bearer <token> on API calls. Format is not standardized by OAuth2 itself — often an opaque string validated via introspection, or a JWT if the AS chooses. Never assume you can decode it client-side; treat it as a credential, not as user info.
For the Client — "here's proof of who authenticated, from us"
Always a JWT, always meant to be read by the client, never sent to an API. Three dot-separated, base64url segments:
- iss
- who signed it
- sub
- the user's stable id
- aud
- your client_id
- exp / nonce
- replay protection
Verify the signature, iss, aud, exp, and — if you sent one — nonce, before you trust any claim inside.
For the Authorization Server only — "let this client ask again later"
Long-lived, never sent to a Resource Server, never sent to the browser in a public-client SPA if avoidable. Exchanged at the token endpoint for a fresh access token (and often a fresh refresh token — rotation) without bothering the user again. Leak this one and it's usually worse than leaking an access token.
Four roles, whatever the flow
Resource Owner
The user
Owns the data. In OIDC terms, this is also the "End-User."
Client
The app
Wants access. "Public" (SPA/mobile, no secret) or "confidential" (server, has a secret). Called the "Relying Party" in OIDC docs.
Authorization Server
The bouncer + notary
Authenticates the user, issues tokens. Called the "OpenID Provider" when it also issues ID tokens.
Resource Server
The API
Accepts the access token, checks its scope, returns data. May be the same server as the AS, or a completely different one.
Scopes ask. Claims answer.
Request time
Scopes
What the client is asking permission for: openid profile email offline_access. Space-separated, sent on the /authorize request.
Response time
Claims
The actual facts returned inside the ID token / from /userinfo because those scopes were granted: name, email, email_verified, picture.
What actually breaks
Click to expand.
Validating the access token like it's the ID token
Skipping state or nonce
state stops CSRF on the redirect back from the AS. nonce (OIDC only) stops replay of a stolen ID token by binding it to this specific browser session. Both are one extra random string — there's no good reason to omit either.Using the implicit flow, or PKCE without the S256 method
code_challenge_method=plain defeats the point of PKCE — always use S256. And the implicit flow's fragment-based tokens end up in browser history and referrer headers; there's no scenario in 2026 where it's the right call for a new build.Trusting an unverified redirect_uri
Forgetting refresh token rotation
OAuth 2.0 · RFC 6749 · 2012 — OpenID Connect · 2014 — OAuth 2.1 folds PKCE-everywhere and drops implicit/password grants