How does the OAuth PKCE flow keep login tokens boring? #
OAuth 2.0 lets users sign in with an account they already own while your app never handles their password. Your app redirects to the provider, the provider authenticates and asks consent, your app exchanges a one-time code for tokens. PKCE extends the flow so clients without a secret stay safe.
Think of it as a valet key. You hand the valet a key that starts the car but opens nothing else, and you take it back when dinner ends. OAuth is that key ritual, formalized: limited scope, limited lifetime, revocable without changing your locks.
This walkthrough follows one sign-in end to end. Each step carries exactly one security job.
Try it: JWT Decoder — which decodes tokens so you can inspect claims here.
How do you start the redirect with code challenge and state? #
Before redirecting, mint two values. A random state token stored server side for this login attempt. And a PKCE pair: a random code_verifier kept locally plus a code_challenge derived from it that travels in the redirect. The redirect carries client ID, redirect URI, scopes, state, and the challenge.
RFC 7636 defines the verifier as a high entropy random string of 43 to 128 characters, with S256 as the challenge method to use. The challenge binds the flow so only the party that started it can finish it, even if the authorization code leaks through logs or history. State plays a different position: on callback, a mismatch rejects the login before any token exchange, which kills cross-site request forgery at the door.
Keep scopes minimal. Profile and email for sign-in, nothing more. Over-scoping depresses consent conversion and widens breach blast radius. Ask for the valet key, not the house keys.
A redirect looks like this in sketch form:
GET https://provider.example/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/auth/callback
&scope=openid%20profile%20email
&state=RANDOM_STATE_TOKEN
&code_challenge=DERIVED_CHALLENGE
&code_challenge_method=S256Google's OAuth docs walk this flow per application type, and the pattern holds across providers: web server apps keep a secret, browser-only apps lean on PKCE instead.
How do you validate the callback and exchange the code? #
The provider returns the user with a code and the state value. Validate state first. Then exchange code plus stored verifier for tokens at the provider token endpoint, server to server. That call authenticates your app and returns access and refresh tokens plus profile claims.
RFC 6749 structures the whole dance as abstract flow: authorization request, authorization grant, token request, token response, then protected resource access. Your implementation is one concrete performance of that script.
Never exchange codes in browser JavaScript with an embedded client secret. That publishes the secret to everyone with dev tools. Public clients use PKCE without a secret. Confidential server apps use both. And validate the redirect URI against registration exactly, since open-redirector mismatches remain a classic takeover vector.
The exchange, sketched:
const tokens = await fetch('https://provider.example/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/auth/callback',
client_id: process.env.OAUTH_CLIENT_ID ?? '',
code_verifier: storedVerifier
})
}).then((r) => r.json());Store the verifier where the exchange can reach it and attackers cannot: server session, encrypted cookie, or short-lived cache keyed to the login attempt. Then delete it after use. Single-use values must die after serving.
How should you store tokens with short lifetimes? #
Access tokens stay short-lived: minutes to an hour. Keep them in memory or HttpOnly, Secure, SameSite cookies. Never park long-lived credentials in localStorage where any injected script can read them.
Refresh tokens live server side, rotate on each use, and invalidate the predecessor so replay signals theft. Rotation needs atomic handling: if two requests race with the same refresh token, one must win and the other must fail closed rather than issuing two valid chains. For traditional web apps, convert the successful exchange into a session row and set a session cookie. Sessions simplify logout, suspension, and audit: deleting one row ends access everywhere. For APIs and mobile clients, issue short-lived tokens with minimal verified claims plus rotating refresh and explicit revocation records.
After login, follow the unglamorous checklist in the OWASP authentication cheat sheet: regenerate session identifiers at login, set proper cookie flags, enforce idle and absolute timeouts, and log authentication events. Delegated login removes password risk. Session hygiene removes everything it leaves behind.
If hand-rolling all this sounds like a month of careful mistakes, that is because it is. Libraries like Better Auth, a framework-agnostic auth library with plugins for two-factor, passkeys, and SSO, exist so your team configures rather than invents. Use boring, reviewed code for the security core. Spend creativity on the product.
| Piece | What it does | Where it lives | Risk if skipped |
|---|---|---|---|
| State token | Blocks forgery | Server session | Login can be forced |
| Code verifier | Binds request to exchange | Server or secure cookie | Stolen code can be replayed |
| Code challenge S256 | Derives from verifier | Redirect URL | No PKCE protection |
| Redirect URI | Exact match check | Provider console | Open redirect takeover |
| Short access token | Limits blast radius | Memory or secure cookie | Long lived stolen token |
| Rotating refresh token | Signals theft on replay | Server store | Silent token theft |
Step 4: how do you handle preview domains and the production split? #
Redirect URIs must match registration exactly, which collides with preview deployments on throwaway hostnames. Handle it the way mature stacks do: resolve callbacks against the host the user started from, and register each stable environment URI with the provider.
Separate development and production OAuth clients entirely. Development tolerates localhost and preview hosts. Production allowlists only real domains. Never share one client ID across both, and never let test users land in production identity tables. Environment separation is cheap. Incident cleanup is not.
What failures will you meet and how do you launch cleanly? #
Invalid grant almost always means the code expired, was already used, or the verifier mismatched. All single-use by design, all fixed by starting a fresh attempt rather than retrying the dead one. Redirect mismatch means registration and runtime disagree on host, path, or scheme; diff them character by character, including trailing slashes. Silent login loops usually mean cookie scoping or clock skew, not provider outages.
Launch checklist, print it: exact redirect URIs registered per environment, state validated on every callback, PKCE with S256 on every public flow, short access lifetimes with rotating refresh, secrets server side only, sessions regenerated at login, and a logout path that kills both the local session and the refresh chain.
Delegated login done this way removes password risk without adding token risk. Valet key in, valet key out, car still yours.
See how BYOB wires auth with Better Auth
What are the trade-offs? #
Learn the redirect plus exchange plus session shape, then ship with a library: Better Auth with D1 in this stack, Google sign in behind a toggle.
| Where the walkthrough path wins | Where hand rolling loses |
|---|---|
| Understanding state, verifier, and exact redirect URIs before trusting magic | Invalid grant and redirect mismatch bugs from expired codes and host drift |
| Short lived access plus rotating server side refresh | Browser stored long lived tokens expose credentials to injected scripts |
| Preview domains resolved per host with stable envs registered | Single callback allowlists and prod splits punish improvised wiring |
Pick library defaults once the checklist is understood. Hand roll only to learn, then delete the custom code.
Who this is for (and who should skip it)? #
This walkthrough helps builders adding Google sign in to a SvelteKit app who want to understand the redirect plus exchange plus session shape before using a library. If you want to follow the auth code flow with PKCE step by step, this gives you the checklist.
Skip hand rolling if you can use a reviewed library for the security core. Teams under time pressure should configure Better Auth or a similar library and spend creativity on product rather than token plumbing.
- Best for developers adding Google sign-in with PKCE without handling passwords.
- Best for startups wiring redirect, callback, and token exchange safely across preview and production.
- Best for freelancers shipping auth for client web apps with short-lived tokens.
What we learned building this? #
BYOB wires Google sign in through Better Auth with a D1 adapter and a one click Google sign in toggle in the Project Control Center, as documented in the BYOB auth guide. Preview domains are handled by reading forwarded host headers in the auth route before passing the request to the Better Auth handler, so OAuth redirect URIs and magic link verify URLs resolve to the host the user started from. That handling is live at https://byob.studio/blog/byob-auth-better-auth-d1 which returns 200 and describes the same wiring.