An interactive crash course

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.

4roles in every OAuth dance
1flow you actually need (auth code + PKCE)
2005year OAuth was born at Twitter/Magnolia
OAuth 2.1the consolidation you should target today
Chapter 01 — The problem

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.
Chapter 02 — Cast of characters

The four roles in every OAuth flow

Every OAuth conversation has exactly four players. Memorize these and the rest of OAuth snaps into focus.

Role 1

Resource Owner

a.k.a. you, the user, the human
The person who owns the data and gets to decide what apps can touch it.
Example: You logging into a Stripe dashboard.
Role 2

Client

a.k.a. the app asking for access
A web app, mobile app, SPA, CLI, or service that wants to act on the user's behalf.
Example: A Notion integration that wants to read your Google Calendar.
Role 3

Authorization Server

a.k.a. the "Sign in with…" page
Authenticates the user, asks for consent, and issues access tokens. Often a service like Auth0, Cognito, or Better Auth.
Example: accounts.google.com
Role 4

Resource Server

a.k.a. the API
The API that hosts the protected data and accepts tokens in the Authorization: Bearer header.
Example: www.googleapis.com/calendar/v3

Fun 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.

Chapter 03 — Choose your weapon

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.

Question 1
Is there a human user involved?

Chapter 04 — The main event

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.

Resource
Owner
You
ClientYour app
Authorization
Server
Google/Auth0
Resource
Server
The API
Chapter 05 — PKCE, the lock and key

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

Click Generate to mint a fresh one
S256 (don't use "plain")

Step 2 — What gets sent where

code_challenge=
code_challenge_method=S256
↓ user logs in, code comes back ↓
code=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.

Chapter 06 — Tokens, three flavors

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.

opaque or JWT treat like a password

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.

rotate on every use never expose to JS

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.

always a JWT verify the signature!

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)
Chapter 07 — The standard endpoints

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.

EndpointMethodPurposeRequired 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.

https://accounts.google.com/.well-known/openid-configuration
{
  "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"]
}
Chapter 08 — Scopes & consent

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
openid — required for OIDC. Tells the server "also give me an ID token".

Reserved OIDC scopes

Standardized by OpenID Connect — every OIDC provider honors these.

  • openid — opt into OIDC, get an ID token
  • profile — name, picture, locale, etc.
  • email — email + verified flag
  • offline_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.

Chapter 09 — Don't get pwned

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.

Fix: generate a random, unguessable 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.

Fix: OAuth 2.1 requires exact-string match. Register every callback you intend to use, including localhost ports.

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.

Fix: use PKCE (now mandatory), expire codes <60s, and burn them on first use.

XSSStoring tokens in localStorage

Any XSS bug instantly exfiltrates every token. Refresh tokens in localStorage are the worst case — they're long-lived.

Fix: for browser apps prefer the BFF pattern (Backend-for-Frontend): tokens live in a HttpOnly cookie on your server, never in JS.

MIX-UPToken audience confusion

Your API blindly trusts any JWT signed by Google — including ones minted for a totally different app.

Fix: always verify the 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.

Fix: rotate refresh tokens on every use; if you ever see a replay of an old one, revoke the whole family.

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.

Fix: Authorization Code + PKCE for everything user-facing. Client Credentials for machine-to-machine. Device flow for TVs.
Chapter 10 — Sibling protocols

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.0OpenID ConnectSAML 2.0
What it answersCan this app do X?Who is logged in?Who is logged in? (enterprise edition)
FormatJSON / form-encodedJSON + JWTXML, signed
TokenAccess 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" buttonsEnterprise SSO (Okta, Azure AD)
Designed forAPIs, mobile, SPAWeb + native loginBrowser-only, enterprise
Built onOAuth 2Independent (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."

Final boss — Quiz time

Prove it.

10 questions. No timer. Each one has an explanation so even when you miss one, you learn.

Cheat sheet

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: Bearer header. 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 /authorize and verifies on the callback. CSRF defense.
Nonce OIDC
Like state but for the ID Token. Sent at /authorize, echoed in the token's nonce claim. 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_secret too.
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.