Federated login and passkeys

Use authorization code with PKCE

Start a modern authorization flow with state and PKCE, validate the callback, and exchange a one-time code without exposing tokens in URLs.

The authorization code flow is the flow you want for login. The user goes to the provider, approves, and comes back to your app with a short-lived code. Your app trades that code for tokens, server to server. Tokens never travel through the browser’s address bar.

PKCE (Proof Key for Code Exchange, pronounced “pixy”) adds one more guarantee. It ties the code to the exact client instance that started the flow. A stolen code becomes worthless.

How PKCE works

Before redirecting, your app creates a random verifier and derives a challenge from it, the SHA-256 hash, base64url-encoded. Only the challenge goes to the provider. The verifier stays with you.

sequenceDiagram
  accTitle: Authorization code flow with PKCE
  accDescr: The client stores a verifier, sends only its challenge through the browser, receives a code, and proves possession of the verifier during the token exchange.
  participant Client
  participant Browser
  participant Provider

  Client->>Client: Create verifier and challenge
  Client->>Browser: Store transaction and redirect
  Browser->>Provider: Authorization request with challenge
  Provider-->>Browser: Redirect with code and state
  Browser->>Client: Callback with code and state
  Client->>Provider: Code plus original verifier
  Provider-->>Client: Access token

When the callback arrives, your app sends the code and the original verifier to the token endpoint. The provider hashes the verifier and compares it with the challenge it saw earlier. Someone who intercepted only the code can’t finish the exchange. They don’t have the verifier.

They look similar, and people confuse them. state is a random value you generate before the redirect and check exactly on callback. It ties the callback to the browser session that started it, and it stops login CSRF, where an attacker tricks you into completing their login on your browser.

PKCE ties the code to the client that started the flow. Use both. Your library and your provider’s docs will tell you the exact parameters.

The transaction

Store the verifier, the state, the chosen provider, and where to send the user afterwards in a short-lived transaction, server-side or in a protected short-lived cookie. Bind it to the authorization server you selected.

If your app supports more than one issuer, validate the callback’s issuer against the transaction, or use a distinct redirect URI per provider. Otherwise a response from provider A can be fed into a transaction started with provider B. That’s a mix-up attack.

Validate the return destination as a safe local path before storing it. Never read a returnTo from the callback query string and redirect there. That’s an open redirect, a favorite phishing tool. And register exact redirect URIs with the provider, no wildcards.

Consume once

A transaction is single-use. Once the callback succeeds, delete it. A second callback with the same state, code, or verifier must fail, even inside the expiry window.

Try this: build a fake provider in your test suite that issues codes and validates verifiers. Then write six tests: wrong state, wrong verifier, wrong issuer, reused code, expired transaction, and an external returnTo URL. Each one must end with no session created and a generic error, not a redirect to the attacker’s site.

Lesson completed