Skip to content
Engineering

OAuth 2.0 and PKCE Walkthrough for Web Apps

BYOB Team

BYOB Team

9 min read

OAuth 2.0 with PKCE lets users sign in through providers like Google while your app never touches passwords. Redirect with a code challenge and state, validate the callback, exchange code plus verifier for tokens, keep access tokens short lived with rotating refresh tokens, and register exact redirect URIs per environment.

Key takeaways

  • • Redirect with a code challenge and state, never with secrets
  • • Validate state, then exchange code plus verifier server side
  • • Keep access tokens short lived and rotate refresh tokens
  • • Register exact redirect URIs per environment, dev separate from prod
  • • Follow OWASP session guidance after login completes
OAuth 2.0 and PKCE Walkthrough for Web Apps

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.

✨
TIP

Try it: JWT Decoder — which decodes tokens so you can inspect claims here.

Try it right here: jwt decoderOpen full tool

Loading the interactive tool… or open it here.

flowchart LR A[Start login] --> B[Redirect with challenge and state] B --> C[Validate callback state] C --> D[Exchange code plus verifier] D --> E[Create session]

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=S256

Google'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:

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

How we picked these

Walked flow steps against OAuth RFCs, Google OAuth docs, OWASP sheet, and Better Auth intro; no login flow was executed live.

Frequently asked questions

What is OAuth 2.0?

A delegation framework letting users grant apps limited access without sharing passwords. The authorization code flow, defined in RFC 6749, is the standard path for web sign in.

What does PKCE add?

It binds the authorization request to the token exchange with a challenge and verifier pair, per RFC 7636, protecting clients that cannot keep a secret.

Where do tokens live afterward?

Short lived access tokens in memory or secure cookies, rotating refresh tokens server side, and session rows in the database for traditional web apps.

Changelog

  • • Added fit guide, comparison table, and hands on notes
  • • Sep 2026 content upgrade, trade-offs section added and statement H2s converted to question form where meaning stayed the same

About the Author

BYOB Team

BYOB Team

The creative minds behind BYOB. We're a diverse team of engineers, designers, and AI specialists dedicated to making web development accessible to everyone.

Ready to start building?

Join thousands of developers using BYOB to ship faster with AI-powered development.

Get Started Free