NDOVU · AUDIO EXPLAINER · LISTEN
Keys: space/K play-pause · J/L jump 10s · arrows on the waveform nudge 5s
The full briefing transcript, timed to the audio. Click any line to jump there; the current line lights up while you listen.
HostSo here's the scenario that keeps OAuth engineers up at night. You tap "Sign in with Google" on some new app. Your browser opens Google's login page, you approve, and the browser bounces back to the app carrying a small, precious thing: an authorization code. That code is a ticket. Redeem it, and you're in. Now imagine an attacker gets hold of that ticket while it flies through your browser - through a custom URL scheme another app registered, a malicious browser extension, a referrer header that leaked. For years, that was enough. Ticket in hand, attacker walks to the token endpoint and redeems it. Game over.
HostThis is the walk-through of the fix. It's called PKCE. The letters P, K, C, E - pronounced "pixy" - and if you build anything with a sign-in button, this is the ten minutes that make the whole flow click.
HostPKCE stands for Proof Key for Code Exchange, and it was published as RFC 7636 in September 2015. The problem it solves is simple to state: some clients cannot keep a secret. A single-page app ships its JavaScript to every visitor - there's nowhere to hide a password. A phone app can be reverse-engineered. Those are "public clients," and the classic authorization-code flow assumed a client secret they simply don't have.
HostPKCE's answer is elegant. Instead of one shared secret, the client mints a fresh pair of keys for every single login. The first key is the code verifier: forty-three characters of pure randomness, generated on the device, and it never leaves the device until the very last step. The second key is the code challenge: you take the verifier, hash it with SHA-256, and encode the result. That's what travels with the initial authorization request.
HostHold that image: the challenge is public, shouted from the rooftops. The verifier is whispered, kept back. The whole security of the flow lives in the gap between those two facts - because only something that can reverse a SHA-256 hash can turn the challenge back into the verifier. And nobody can.
HostWatch the first hop on the wire. The client builds the authorization URL you already know - response type code, client ID, redirect URI, scope, state - and adds two new parameters: codechallenge, and codechallengemethod set to S256. That tells the server "I'm proving ownership later, by hash."
HostThe authorization server takes the challenge and files it away, stapled to this login session. Remember that - the server remembers the challenge. The client then does what it always does: it redirects your browser to the server's login page.
HostAnd here's the part people miss. From this moment on, the client is out of the loop. You type your password on the server's page, not the app's. You approve the scopes on the server's page. The client never sees your credentials, never handles your password, never gets to impersonate you. That's the delegation bargain at the heart of OAuth - and PKCE slots into it without touching it.
HostThe server approves you and redirects the browser back to the client's redirect URI with a one-time authorization code and that state value echoed back. Before the client does anything with the code, it checks the state. Is this the same random value I sent out? If yes, this callback answers my request. If no, stop - because a mismatched state means someone may have spliced their own response into your flow, a classic cross-site-request-forgery move against login.
HostIt's a small check with a big job, and it costs nothing. Skip it, and an attacker can inject a code from their own session and quietly link their identity to your account.
HostNow the moment the whole design protects. The client walks up to the token endpoint - directly, over a back channel - and says: here's the code, and here's my code verifier. The server pulls out the challenge it stapled to that code, hashes the verifier you just handed over, and compares. Match? Here's your access token. Mismatch? Invalid grant, full stop.
HostRun the attacker's play against this. They intercepted the code in the browser - fine, they have the ticket. They walk to the token endpoint, and the server asks for the verifier. They don't have it. It never crossed the wire. The hash they saw is not reversible. The exchange fails, the code expires, the attack is dead. That's the entire trick: a code without its verifier is a ticket without a barcode.
HostThree things worth carrying home. First: always S256. The spec allows a "plain" method where the verifier travels as its own challenge - that survives only for legacy servers, and it throws away the protection the moment the authorize request leaks. Second: generate a fresh pair for every login. Reusing a verifier across sessions quietly rebuilds the single-secret problem PKCE was invented to remove. Third: the challenge is safe to log, safe to cache, safe to send anywhere. The verifier is the one thing your logs must never see.
HostAnd notice what PKCE did not change. The roles are the same, the code is the same, the token endpoint is the same shape. It slid one extra handshake into a flow you already know, and turned a stolen code from a disaster into dead weight.
HostSo the next time a sign-in button asks for your attention, you know the choreography behind it: two keys minted in your browser, a challenge shouted ahead, a verifier saved for last. If you want to feel it rather than picture it, the PKCE console on this lab-site runs the entire flow - keys, redirect, sign-in, state check, exchange - with tamper buttons that let you attack every defense yourself. Go break it. It holds.
HostThat's PKCE. Walk safe.