Handbooks  /  OAuth & Auth Deep Dive
Handbook~16 min readSecurityworked math + runnable code
The OAuth & Auth Deep Dive

Never hand over
the password.

"Sign in with Google" solves a problem that sounds impossible: let some app act for you — read your calendar, post on your behalf — without ever telling it your Google password. OAuth's answer is a piece of misdirection: the app never gets your credentials, only a short-lived code it swaps for a limited, revocable token. And because a code flying through a browser could be stolen or forged, two small parameters — state and PKCE — stand guard. This handbook is the flow, and the exact reason those two checks make it safe.

01

Delegated access

OAuth 2.0 exists to answer one question: how can I let an app do something on my behalf at another service without giving it my password? Handing over credentials would be a disaster — the app could do anything, forever, and you couldn't revoke it without changing your password everywhere. OAuth replaces that with delegated authorization: you authenticate directly with the provider (Google, GitHub), approve a specific, limited scope, and the provider hands the app a token — a key to one room, not the master key.

The token is scoped (only what you approved — "read calendar," not "delete account"), short-lived, and revocable (you can cut it off without touching your password). That's the whole value proposition: your credentials never leave the provider, and the access you grant is narrow and reversible. But getting that token to the app safely, through the untrusted medium of a browser redirect, is where the real design lives — and where the security is won or lost.

The one-sentence version

OAuth gives an app a scoped, revocable token instead of your password — delivered via a short-lived code that's worthless without a back-channel exchange, guarded by state (blocks forged responses) and PKCE (blocks stolen codes).

02

The code flow

The standard, most secure path is the authorization code flow. Its defining trick is that what travels through the browser is not the token — it's a short-lived authorization code that's useless on its own. Turning that code into a token requires a separate back-channel request the browser (and any attacker watching it) can't make.

Authorization code flow (with PKCE)
1 · RedirectApp sends you to the provider with a state + a PKCE challenge.
2 · Log inYou authenticate with the provider and approve the scope. The app never sees this.
3 · Code backProvider redirects to the app with a short-lived code + the echoed state.
4 · ExchangeApp calls the provider back-channel with the code + its secret + PKCE verifier → gets the token.

Two things never appear in a browser URL: your password (you type it into the provider, not the app) and the access token (it's only ever returned over the back channel in step 4). The code does ride through the browser, which is exactly why it must be short-lived, single-use, and — as the next section shows — bound to a secret the attacker can't have. Split the flow into "get a code publicly, redeem it privately" and the browser leg carries nothing an attacker can use alone.

03

State & PKCE

Two parameters defend the two ways the browser leg can be attacked. state is a random value the app generates before the redirect and checks is echoed back unchanged. If the returned state doesn't match what the app sent, the response belongs to a flow the app never started — a forged authorization response — and it's rejected. That's CSRF protection: an attacker can't trick your browser into completing their login into your session, because they can't guess your state.

PKCE (Proof Key for Code Exchange, "pixie") defends against a stolen code. Before redirecting, the app generates a random verifier and sends only its hash — the challenge — to the provider. At exchange time (step 4), the app must present the original verifier; the provider re-hashes it and rejects the exchange unless it matches the challenge it stored. So even if an attacker intercepts the authorization code, they can't redeem it: they'd need the verifier, which never left the app. State proves the response is yours; PKCE proves the redemption comes from the same app that started the flow. Together they make a code that flies through an untrusted browser safe to use.

04

The verification math

PKCE binds the code to a secret via a one-way hash: the challenge is the hash of the verifier, and the exchange re-derives it and compares. A token is issued only if the state matches and the verifier hashes to the challenge:

challenge  =  base64url(SHA256(verifier))     issue token  ⟺  (statesent = stateback) ∧ (SHA256(verifier) = challenge)

The challenge is public (it rode through the browser); the verifier is secret. Because SHA-256 is one-way, seeing the challenge doesn't reveal the verifier — so a stolen code can't be redeemed.

Break either check and no token is issued — a mismatched state (forged response) or a wrong verifier (stolen code without the secret) both fail closed:

statesent ≠ stateback  ⟹  reject (CSRF)     SHA256(verifier) ≠ challenge  ⟹  reject (stolen code)

Two independent gates, both must pass. The runnable version below derives the challenge, checks state, verifies PKCE, and runs the full exchange for valid and attacker cases.

RUN IT YOURSELF

The auth-code exchange, verified

OAuth's authorization-code flow is guarded by two checks. State is a random value echoed back unchanged — if it doesn't match, the response was forged (CSRF) and is rejected. PKCE binds the code to a secret: the app sends up front a challenge = SHA-256 hash of a secret verifier, and at token exchange must present the verifier, which the provider re-hashes and compares. Because the hash is one-way, seeing the public challenge doesn't reveal the verifier — so a stolen code is useless. A token is issued only if state matches AND the verifier hashes to the challenge. Change the verifier or the state and watch the exchange fail closed.

CPython · WebAssembly
05

Authentication vs authorization

The single most common confusion in this space is treating authentication and authorization as the same thing. They aren't. Authentication (authn) is proving who you are — verifying identity with a password, passkey, or biometric. Authorization (authz) is deciding what you may do — whether an already-identified user can perform a given action. You authenticate once; you're authorized (or not) for each thing you try.

Plain OAuth 2.0 is an authorization protocol — it grants scoped access via tokens; it does not, by itself, tell the app who you are. That's what OpenID Connect (OIDC) adds: a thin identity layer on top of OAuth that issues an ID token (a signed JWT) proving who authenticated. "Sign in with Google" is OIDC — OAuth's access grant plus an identity assertion. Keep the two straight and the ecosystem stops being a soup of acronyms: OAuth hands out access, OIDC adds identity, tokens carry both across requests so the server doesn't have to keep server-side session state. Confuse them and you'll build systems that authenticate but don't authorize (everyone's an admin) or authorize without authenticating (anonymous access to private data) — both classic breaches.

06

Pitfalls

The first is skipping state or PKCE because "it works without them." It does work — right up until someone forges a response or steals a code. Both parameters are cheap and both are now mandatory in the current best-practice (OAuth 2.1 bakes PKCE into every flow). The second is the deprecated implicit flow, which returned the token directly in the browser URL. It exists in old tutorials; don't use it — the authorization-code flow with PKCE replaces it precisely because a token in a URL is a token in browser history, referer headers, and logs.

Three more. Not validating tokens: a server that accepts a token without checking its signature, issuer, audience, and expiry will accept forged or stolen ones — always verify, don't just decode. Wide scopes and no expiry: request the least scope you need and keep access tokens short-lived (use refresh tokens to renew), so a leaked token is both limited and temporary. And leaking tokens: a token is a bearer credential — whoever holds it is you — so treat it like a password: never log it, never put it in a URL, store it carefully (httpOnly cookies over localStorage where you can). Get the flow right — code not token in the browser, state and PKCE always on, tokens validated, scoped, and short-lived — and OAuth does something genuinely elegant: it lets strangers act for you, exactly as much as you allowed, and never a bit more. The whole handbook is that sentence: delegate access, never the password, and guard the code with state and PKCE.

Worth knowing

Use the authorization-code flow with PKCE (never implicit), always send and check state, validate every token's signature/issuer/audience/expiry, request least scope, and keep access tokens short-lived with refresh tokens. Treat a token as a bearer credential — holding it is being you.

Frequently asked

Quick answers

What is OAuth?

A protocol that lets an app act for you at another service via a scoped, revocable token — without ever seeing your password.

What's the code flow?

You log in with the provider, which redirects a short-lived code to the app; the app exchanges it back-channel for a token. The token never touches a browser URL.

What do state and PKCE do?

State blocks forged (CSRF) responses by echoing a random value; PKCE binds the code to a secret verifier so a stolen code can't be redeemed.

Authn vs authz?

Authentication proves who you are; authorization decides what you may do. OAuth is authz; OIDC adds authn with an ID token.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.