Integration Lab / module 01 — oauth checking…
Module 01 · Live

OAuth 2.0, including the parts that break.

Most OAuth walkthroughs stop at “you got a token.” That’s the easy half. The half that costs you a weekend is the token expiring mid-transaction, the user revoking consent in their Google settings, and the authorization code that gets replayed. Connect your own account below and trigger each one.

Connection
Trigger a failure

These hit the deployed callback handler for real and render whatever it returns.

oauth · token lifecycle
idle
Expires in
Refreshes
0
Last latency
The flow

Authorization code, with PKCE

Six steps. The two that matter are the ones most implementations skip: generating a verifier before you leave, and proving you hold it when you come back.

BROWSER GOOGLE EDGE FUNCTION 1 · /api/oauth/start verifier = random(32) challenge = S256(verifier) 2 · consent screen holds the challenge user approves scope 3 · /api/oauth/callback receives code + state state compared constant-time 4 · token exchange code + verifier + secret → access + refresh token no verifier = no tokens 5 · signed session HttpOnly · Secure 1 hour, then gone nothing stored server-side 6 · refresh before expiry, not after rotated token replaces old revoked → reconnect
Why PKCE

The attack it exists to stop

Without PKCE, the authorization code is a bearer token in transit. It arrives back at your app through a redirect — a URL, which lands in browser history, server logs, and Referer headers, and passes through whatever is between the user and you.

An attacker who captures that code can exchange it for real tokens, because the token endpoint only asks for the code and your client ID. On a mobile app or a single-page app, where the client secret can’t be kept secret, that’s the whole attack.

PKCE closes it by making the code useless on its own. Before redirecting, the app generates a random verifier, hashes it, and sends only the hash — the challenge. Google stores the challenge with the pending authorization. When the code comes back, the app must present the original verifier; Google hashes it and compares. A stolen code without the verifier exchanges for nothing.

The verifier never leaves your side of the connection, so an attacker who sees the redirect sees only a code they cannot spend. It costs about ten lines, and it is specified in RFC 7636.

Failure modes

Five ways this breaks, and what the code does about each

An integration that only handles the happy path works in the demo and pages you at 3am. Every row below is handled explicitly rather than collapsing into a generic 500.

What happensSymptomHow it’s handled
User cancels consent ?error=access_denied on the callback Detected before anything else is read. Redirects back with the reason, PKCE cookie cleared. Not an error state — a choice the user made.
State mismatch Returned state ≠ the one issued Compared in constant time, then rejected. A === on a secret leaks timing information; this is the same discipline as comparing a signature.
Code replayed or expired Google returns invalid_grant Surfaced by name rather than as a 500, because invalid_grant means something specific — the code was already spent or is stale, and the fix is to restart the flow, not to retry.
Access token expires mid-session 401 from the resource API Avoided rather than caught. The session tracks absolute expiry and refreshes when under five minutes remain, so the expiry never lands in the middle of a user’s request.
User revokes consent in Google settings invalid_grant on refresh Session cleared and the UI asks for reconnection. Retrying a revoked grant never succeeds, so retry logic here is a bug, not resilience.
Implementation

The actual source

Public repo — read the handling rather than taking the table above on trust.

FileWhat’s in it
_shared.ts PKCE generation, HMAC-signed cookies, constant-time compare. No dependencies — Workers’ WebCrypto only.
start.ts Builds the authorization URL, puts the verifier and state in a signed 10-minute cookie. Nothing stored server-side.
callback.ts Code exchange, plus the four distinct failure branches from the table above.
session.ts Metadata-only inspector, forced refresh with rotation, and a disconnect that revokes upstream — clearing a cookie alone leaves the grant alive in the user’s account.
config.ts Reports which variables are missing, by name only. A config endpoint that leaks what it checks for is worse than none.

← Back to the lab