OAuth, decoded.
In one scroll_
Animated flows, a live PKCE generator, a real JWT decoder, and a quiz at the end. By the time you hit the bottom, you'll talk OAuth like you wrote the RFC.
Why we needed OAuth in the first place
Before OAuth, if a third-party app wanted to read your Gmail or post to your Twitter, you had to give it your password. That meant any random calendar integration could now log in as you, change your password, and read your DMs forever. That is the era OAuth ended.
Before OAuth — "the password antipattern"
- App stores your real password (often in plaintext).
- App has full access to your account, not just what it needs.
- You can't revoke access without changing your password everywhere.
- 2FA? Forget it — the app can't replay your TOTP.
- If the app gets breached, attackers get your password for every site you reused it on.
After OAuth — "delegated authorization"
- App never sees your password — you log in at the real provider.
- You grant specific scopes (e.g.
read:email) — nothing more. - You can revoke the app's access any time, password unchanged.
- 2FA happens at the provider, where it belongs.
- If the app is breached, attackers get a short-lived token — not your whole identity.
The four roles in every OAuth flow
Every OAuth conversation has exactly four players. Memorize these and the rest of OAuth snaps into focus.
Resource Owner
Client
Authorization Server
accounts.google.comResource Server
Authorization: Bearer header.www.googleapis.com/calendar/v3Fun fact: the Authorization Server and the Resource Server can be the same machine — they're roles, not necessarily separate servers. At Google they're different teams; at a small startup they may live in the same Express app.
Which OAuth flow should I use?
OAuth 2 has several "grant types" (a.k.a. flows). Most of them are obsolete in 2026. Answer the questions below — we'll tell you which one you actually need.
Authorization Code flow, step by step
This is the flow you'll use 95% of the time. The animation below walks through every hop. Click Next step to advance — watch the messages move between the four roles.
OwnerYou
ServerGoogle/Auth0
ServerThe API
Try PKCE live — your browser, real crypto
PKCE ("pixy" — Proof Key for Code Exchange) stops attackers who steal the
authorization code from being able to redeem it. Your client invents a secret
(code_verifier), hashes it (code_challenge),
sends the hash up-front, and reveals the original secret only at token exchange.
No verifier? No token.
This panel runs the same SubtleCrypto SHA-256 your real client would. Hit generate and watch.
Step 1 — Client generates a verifier
S256 (don't use "plain")Step 2 — What gets sent where
code_challenge=—
code_challenge_method=S256code=authorization_code_here
code_verifier=—
Server hashes the verifier with SHA-256, base64url-encodes it, and compares to the
challenge it stored at /authorize. Match → tokens. Mismatch → reject.
! Why PKCE matters even for confidential clients
Originally PKCE was designed for "public" clients (mobile/SPA) that can't keep a secret. But in OAuth 2.1 it's mandatory for everyone, because the authorization code can leak through browser history, server logs, or referer headers. PKCE makes a stolen code worthless without the verifier — which never leaves the client's memory.
Access, Refresh, ID — which is which?
A Access Token
Short-lived (minutes to an hour). Presented to the Resource Server in the
Authorization: Bearer … header to call APIs.
R Refresh Token
Long-lived (days to months). Sent only to the Authorization Server's
/token endpoint to mint a new access token without re-prompting the user.
I ID Token
OIDC only. A signed JWT about the user (their sub,
email, name). Lets the client say "I know who logged in" without calling an API.
Decode a JWT yourself
Most ID tokens and many access tokens are JWTs — three base64url chunks separated by dots. Paste one below (or try a sample) to see the header, payload, and signature. Nothing leaves your browser.
Header
—
Payload
—
Signature (not verified here — needs the issuer's public key)
—
The well-known URLs you'll see everywhere
Authorization servers expose a small, standardized set of endpoints. Once you know their names, every provider — Google, GitHub, Auth0, Cognito, your homegrown one — looks the same.
| Endpoint | Method | Purpose | Required params |
|---|---|---|---|
| /authorize | GET | Front-channel. Browser redirect that authenticates the user and asks consent. | response_type, client_id, redirect_uri, scope, state, code_challenge |
| /token | POST | Back-channel. Exchanges an authorization code (or refresh token) for tokens. | grant_type, code, redirect_uri, client_id, code_verifier |
| /revoke | POST | Revoke an access or refresh token before its natural expiry. RFC 7009 | token, token_type_hint |
| /introspect | POST | Resource server asks "is this token still valid and what does it grant?" RFC 7662 | token (+ client auth) |
| /userinfo | GET | OIDC. Returns claims about the authenticated user. | Authorization: Bearer <access_token> |
| /device_authorization | POST | Device flow. Returns a user code + verification URL for "enter this code on your phone". RFC 8628 | client_id, scope |
| /.well-known/openid-configuration | GET | OIDC discovery document. JSON listing every other endpoint, supported scopes, and JWKS URL. | — |
| /.well-known/oauth-authorization-server | GET | Pure OAuth 2 discovery (OIDC's sibling). RFC 8414 | — |
| /.well-known/jwks.json | GET | Public keys used to verify JWT signatures. | — |
Discovery in action
Every OIDC provider exposes this magic URL. Fetch it once, cache the JSON, and you never have to hardcode endpoint paths again.
{
"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",
"response_types_supported": ["code", ...],
"scopes_supported": ["openid", "profile", "email", ...],
"code_challenge_methods_supported": ["plain", "S256"]
}
Scopes are how you say "only this much"
Scopes are space-separated permission strings the client requests at /authorize.
The user sees them on the consent screen ("This app wants to read your email and post on your behalf").
Pick scopes below and watch the request build.
GET https://auth.example.com/authorize?
response_type=code&
client_id=demo-app&
redirect_uri=https://app.example.com/cb&
scope=openid&
state=abc123&
code_challenge=…&
code_challenge_method=S256
Reserved OIDC scopes
Standardized by OpenID Connect — every OIDC provider honors these.
openid— opt into OIDC, get an ID tokenprofile— name, picture, locale, etc.email— email + verified flagoffline_access— get a refresh token too
Provider-specific scopes
Anything beyond the OIDC core is a free-for-all the provider invents. Common conventions:
- GitHub:
repo,user:email,admin:org - Google:
https://www.googleapis.com/auth/calendar.readonly - Slack:
chat:write,channels:read
There's no universal scope namespace. Read the provider's docs.
The security gotchas (and how to dodge them)
Most OAuth bugs aren't in OAuth — they're in how people implement it. Here are the classics that the OWASP and Auth0 security teams keep finding in the wild.
CSRFMissing or unchecked state parameter
Without state, an attacker can trick a logged-in victim into completing
their OAuth flow — linking the attacker's social account to the victim's app account.
state, store it in a cookie or session, and reject the callback if it doesn't match.REDIRLoose redirect_uri matching
Allowing wildcard or substring matches on redirect_uri lets attackers ship the auth code to https://evil.com/?evil-callback.
LEAKAuth code in browser history / referer
The code lands in the URL, then sticks around in history, server logs, and any link the user clicks afterward.
XSSStoring tokens in localStorage
Any XSS bug instantly exfiltrates every token. Refresh tokens in localStorage are the worst case — they're long-lived.
MIX-UPToken audience confusion
Your API blindly trusts any JWT signed by Google — including ones minted for a totally different app.
aud (audience) and iss (issuer) claims on every JWT, not just the signature.ROTLong-lived refresh tokens never rotated
A leaked refresh token = persistent access for the attacker, forever.
FLOWUsing the Implicit or Password flow in 2026
Both flows are removed in OAuth 2.1. Implicit puts tokens in URL fragments (logs, history). Password flow trains users that handing passwords to apps is normal.
OAuth vs OIDC vs SAML — what's the difference?
The single most-asked OAuth question. Short version: OAuth is authorization (what the app can do). OIDC and SAML are authentication (who the user is). OIDC is a thin layer built on OAuth.
| OAuth 2.0 | OpenID Connect | SAML 2.0 | |
|---|---|---|---|
| What it answers | Can this app do X? | Who is logged in? | Who is logged in? (enterprise edition) |
| Format | JSON / form-encoded | JSON + JWT | XML, signed |
| Token | Access token (opaque or JWT) | Adds an ID Token (always JWT) | SAML Assertion (XML) |
| Where you see it | "Connect your GitHub" buttons | "Sign in with Google" buttons | Enterprise SSO (Okta, Azure AD) |
| Designed for | APIs, mobile, SPA | Web + native login | Browser-only, enterprise |
| Built on | — | OAuth 2 | Independent (SOAP era) |
? Common confusion: "Sign in with Google" uses OAuth, right?
Sort of. It uses OpenID Connect, which is an authentication layer
built on top of OAuth 2's authorization code flow. The redirect dance is identical — but
the client also asks for the openid scope, gets back an
ID Token alongside the access token, and uses that to know who the user is.
Without OIDC, OAuth alone tells you nothing about the user — just that "the bearer of this token
is allowed to do X."
Prove it.
10 questions. No timer. Each one has an explanation so even when you miss one, you learn.
OAuth glossary you can pin to your monitor
- Authorization Code code
- A short-lived (typically <60s) opaque string returned to the client's redirect URI. Useless on its own — must be exchanged at
/token. - Access Token AT
- The credential a client presents to a resource server in the
Authorization: Bearerheader. Short-lived (minutes to ~1h). - Refresh Token RT
- Long-lived credential sent only to the authorization server to get fresh access tokens without re-prompting the user.
- ID Token IDT
- OIDC-only JWT containing claims about the authenticated user. Tells the client who logged in.
- PKCE RFC 7636
- Proof Key for Code Exchange. A SHA-256 hash dance that prevents auth-code interception attacks. Mandatory in OAuth 2.1.
- State Parameter state
- Random per-request value the client sends to
/authorizeand verifies on the callback. CSRF defense. - Nonce OIDC
- Like
statebut for the ID Token. Sent at/authorize, echoed in the token'snonceclaim. Prevents token replay. - Scope scope
- Space-separated permission string. What the user consents to and what the resulting token can access.
- Front-channel
- Communication that goes through the user's browser (URL redirects, fragments). Visible to JS, logged by browsers.
- Back-channel
- Server-to-server HTTPS POST. Not visible to the user or the browser. Where secrets and tokens belong.
- JWKS jwks_uri
- JSON Web Key Set — the public keys you use to verify a JWT's signature. Fetched from the provider's discovery document.
- Confidential vs Public client
- Confidential = can keep a secret (server-side app). Public = cannot (SPA, mobile, CLI). Public clients must use PKCE; confidential ones authenticate with
client_secrettoo. - BFF (Backend-for-Frontend)
- Pattern where a small server-side proxy holds the tokens and issues only HttpOnly session cookies to the browser. Best practice for SPAs in 2026.
- DCR RFC 7591
- Dynamic Client Registration. Lets clients register themselves with the authorization server at runtime. Used by MCP, IDE plugins, etc.