From the Production paths series
OAuth That Survives Localhost, Preview URLs and Production
Design one explicit identity contract across local development, temporary previews and production—without loose callbacks or unsafe account linking.
By BlinkHost Engineering · Published 01/09/2026
What you will understand
- — Map frontend, API, provider and callback origins for every environment.
- — Apply one-use state, PKCE, nonce and explicit account-linking rules.
- — Diagnose callback, cookie and session failures at the correct boundary.
OAuth failures are often described as provider problems. Most are environment-contract problems.
The application starts on http://localhost:3000, its API listens on another port, a pull request creates a temporary preview hostname, and production uses two custom domains. Somewhere in that journey a callback is registered with the wrong slash, a cookie belongs to the wrong host, or a preview URL is accepted with a wildcard broader than anyone intended.
The result is familiar: the provider says redirect_uri_mismatch, the callback loses its session, or sign-in works locally and fails after deployment.
This guide builds the flow from one principle: every environment needs an explicit identity contract.
Begin with the four actors
Write down the actors before opening a provider dashboard:
| Actor | Example | Responsibility |
|---|---|---|
| Browser application | https://app.example.com |
Begins sign-in and receives the final application result |
| Authentication backend | https://api.example.com |
Creates state, exchanges the code and establishes the session |
| Identity provider | Google, GitHub or Microsoft | Authenticates the user and returns an authorization response |
| Protected API | Often the authentication backend | Verifies the resulting application session on later requests |
The provider callback normally belongs to the authentication backend because that is where the client secret, PKCE verifier and account-linking policy can be protected. The frontend may have its own internal /auth/callback page, but that is the destination after the backend has validated the provider response.
Build an environment matrix
Do not maintain callback URIs in memory or scattered chat messages. Keep a small matrix:
| Environment | Frontend origin | API origin | Provider callback |
|---|---|---|---|
| Local | http://localhost:3000 |
http://localhost:8000 |
http://localhost:8000/api/auth/oauth/google/callback/ |
| Preview | https://pr-142.preview.example.net |
https://preview-api.example.net |
https://preview-api.example.net/api/auth/oauth/google/callback/ |
| Production | https://app.example.com |
https://api.example.com |
https://api.example.com/api/auth/oauth/google/callback/ |
Register exact callbacks whenever the provider allows it. A wildcard such as https://*.preview.example.net/callback expands the set of origins able to receive an authorization response. If previews are untrusted or contributor-controlled, they should not share a production OAuth application.
A safer preview strategy uses one stable authentication callback. The state record carries the approved preview destination, and the backend redirects there only after validating it against a narrow allowlist. Never accept an arbitrary next=https://... value from the browser.
Create state on the server
Before redirecting to the provider, the backend creates a short-lived, one-use transaction record containing:
- a cryptographically random state value or its digest;
- the intended operation: sign in, register or link;
- the approved destination after completion;
- a PKCE verifier where supported;
- an OIDC nonce when an ID token will be accepted;
- a binding to the initiating browser session;
- an expiry measured in minutes, not days.
The browser receives only what it needs for the authorization request. Provider access tokens and client secrets do not pass through frontend code.
OAuth 2.0 Security Best Current Practice, published as RFC 9700 in January 2025, recommends authorization-code flows, PKCE and exact redirect matching. It also explains why open redirectors and weak redirect comparison become token-exfiltration paths.
Treat the callback as hostile input
The callback endpoint receives values from the browser and provider. Validate them in a fixed order:
- Reject provider errors with a bounded, user-safe reason.
- Require the expected code and state parameters.
- Load the matching unexpired transaction.
- Atomically mark it consumed so replay loses the race.
- Confirm the initiating browser-session binding.
- Exchange the code using the exact callback URI used in the authorization request.
- Validate issuer, audience, signature, expiry and nonce for OIDC tokens.
- Fetch or verify the provider subject and required email status.
- Apply explicit account-linking rules.
- Establish the application session, then redirect to the stored safe destination.
Do not log the authorization code, access token, ID token or PKCE verifier when any step fails.
Account linking needs a policy of its own
Suppose a password account uses ada@example.com and Google returns the same verified email. Automatically merging them feels convenient. It can also attach a new external identity to an existing account without proving that the current user intended to do so.
A conservative design separates three journeys:
- New sign-in: a known provider subject signs in to its bound account.
- New registration: an unknown subject creates an account after required legal and product steps.
- Linking: an already authenticated user explicitly connects a provider and confirms the operation.
The stable external key is the pair (provider, subject), not the email address. Email can help with communication and conflict detection, but it should not silently rewrite identity ownership.
Cookies across two origins
When frontend and API use different origins, cookie behaviour becomes part of the protocol.
Prefer the smallest practical cookie scope. Use Secure and HttpOnly; choose SameSite based on the actual cross-site journey rather than copying a framework default. If frontend and API are same-site subdomains, SameSite=Lax may support the top-level OAuth return while reducing cross-site exposure. A genuinely cross-site embedded flow may require SameSite=None; Secure, which also requires a stronger CSRF design.
For credentialed cross-origin API calls:
- allow only known frontend origins;
- never combine credentials with
Access-Control-Allow-Origin: *; - include credentials deliberately in the browser request;
- protect state-changing requests with CSRF controls appropriate to the session design;
- ensure proxies preserve the original HTTPS scheme so secure callback construction is correct.
Local development is the exception where providers may allow HTTP on loopback hosts. Do not generalize that exception to shared previews.
Diagnose by locating the broken boundary
| Symptom | First things to compare |
|---|---|
redirect_uri_mismatch |
Byte-for-byte callback in start request, token exchange and provider registration |
invalid_state |
Cookie presence, state expiry, one-use consumption and host changes |
| Callback succeeds, app still signed out | Session cookie domain, Secure, SameSite, CORS credentials and proxy scheme |
| Existing account is blocked | Provider-subject binding and explicit linking policy |
| Works locally, fails in preview | HTTPS, preview allowlist, stable callback and environment-specific client credentials |
| Intermittent failure | Multiple callback attempts, clock skew, load-balanced session state or stale configuration |
Capture a correlation identifier and coarse stage name—start, state, exchange, identity, session—without capturing credentials. That is enough to distinguish most failures.
A production acceptance test
Before enabling a provider, exercise:
- successful new registration;
- successful return to a safe intended page;
- user denial at the provider;
- expired and replayed state;
- callback in a second browser session;
- existing-account conflict;
- explicit link and unlink;
- disabled local account;
- provider timeout;
- an unapproved preview destination;
- cookies after a real cross-origin production callback.
Mock-provider tests are valuable for deterministic failure paths. They are not a substitute for one controlled end-to-end journey through each real provider, because provider configuration is part of the system.
The durable solution is not another redirect workaround. It is a small, documented contract connecting origins, callbacks, session policy and identity ownership in every environment.
Further reading
Build an exact callback URI
Register this exact callback
https://api.example.com/api/auth/oauth/google/callback/This helper constructs a BlinkHost-style example. Always use the callback contract documented by the application you are configuring.
OAuth environment matrix
A CSV template for recording frontend origins, API origins and exact provider callbacks.
Download oauth-environment-matrix.csvDisclosure: The URI helper constructs a BlinkHost-style example and does not replace the callback contract of the application being configured.