OAuth guides

PKCE, state, and redirect URIs for safer OAuth apps

Core defenses against authorization code interception, CSRF on the callback, and redirect manipulation.

7 min read

OAuth flows cross trust boundaries: the browser, your servers, and a third-party authorization server. A handful of parameters exist specifically to close gaps that appear when any of those steps is attacked or misconfigured.

Proof Key for Code Exchange (PKCE)

PKCE protects the authorization code step when the client cannot keep a secret: single-page apps, mobile apps, and native desktop apps. Before redirecting to authorize, the client generates a random code_verifier and sends a code_challenge (usually SHA-256 of the verifier) on the authorize request. The token exchange must include the original code_verifier; the server verifies it against the stored challenge.

An attacker who intercepts the authorization code in the redirect still cannot exchange it without the verifier. Use PKCE even for confidential server apps; many providers require or recommend it for all clients.

// Simplified idea (use your library's helpers in production)
const verifier = randomString(64)
const challenge = base64url(sha256(verifier))

// Authorize URL includes:
//   code_challenge=challenge
//   code_challenge_method=S256

// Token request includes:
//   code_verifier=verifier

The state parameter

state is an opaque value your app generates before redirecting to the authorization server. The server returns the same value on the callback. Your app must verify that it matches the value stored in the user session (cookie or server-side store).

That binding prevents CSRF on login: an attacker cannot start a flow and trick a victim's browser into finishing it on the victim's account in your app. state is not a substitute for PKCE; use both where applicable.

Redirect URI rules

  • Register exact redirect URIs; avoid open redirects on your callback route.
  • Do not use http:// except for loopback hosts (e.g. http://127.0.0.1:3000/callback) where the provider allows it.
  • Custom URL schemes for mobile need careful validation so another app cannot register the same scheme on a compromised device.
  • Path and query must match what you registered; some providers are strict about trailing slashes.

Client secrets and token storage

Confidential clients (typical server apps) use a client secret only on the server during the code exchange. Never embed secrets in front-end bundles or mobile binaries.

Store refresh tokens hashed or encrypted, rotate on use when the provider supports it, and revoke on logout when the provider offers revocation endpoints. Log authorization errors without printing full tokens.

Dev and CI without weakening security

Register a separate OAuth client on your mock IdP with loopback redirect URIs and the same scopes as production. Your app runs on localhost; authorize and token calls go to the mock issuer. Automated tests use a headless browser against that same issuer so CI does not depend on Google or GitHub rate limits.

Keep validating JWTs against the issuer's JWKS URL, checking state, and using PKCE when your production client does. Security regressions should surface before you swap OAUTH_ISSUER to a real provider.