# Redirect, exchange, profile: trace the login boundary

A successful redirect is only the middle of a login. Follow the browser, backend, AnyOAuth, and provider through a numbered contract that ends with your own user record and session.

By Gabe. Published 2026-10-06.
Author: https://anyoauth.com/blog/authors/gabe/
Source: https://anyoauth.com/blog/redirect-exchange-profile/

1. Browser → AnyOAuth → provider: redirect with transaction protection
2. Your backend → AnyOAuth: redeem the handoff code once
3. Your backend → AnyOAuth: retrieve the profile, then resolve a local user and create the app session

The three service operations provide identity data. Your application then creates its own session.

Use an artifact inventory beside the handler you're reviewing. You should be able to point to the project client ID, backend secret, saved callback, state, verifier, handoff code, profile token, and application session cookie. If several variables are simply called `token`, rename them before debugging. Their issuers, recipients, and lifetimes differ.

The following trace is a conceptual integration contract, not runnable code or a report of a test run. It follows the [quickstart](/docs/) so you can compare a real implementation, including one drafted by a coding assistant, against each boundary.

## 1. Save the transaction before redirecting the browser

Your backend must preserve the transaction it will need when the browser comes back. Create a fresh state and S256 PKCE pair. Save the state, original verifier, expiry, and exact registered callback in a short-lived server-side session tied to the initiating browser. If there's a post-login destination, save an allowlisted internal path separately.

Then send that browser to `/authorize` with the public project client ID, selected configured provider, callback, state, and S256 challenge. Keep the project secret and original verifier out of that URL. Google and GitHub are the supported choices; a provider also needs to be enabled in the service configuration.

The challenge is derived from the verifier, not a replacement for saving it. A newly generated verifier at callback time won't prove possession of the original one. The [PKCE reference](/blog/pkce/) explains that binding; [authorization code flow](/blog/authorization-code-flow/) supplies the general redirect/exchange model.

## 2. Separate the provider callback from your callback

AnyOAuth completes the provider-side exchange before issuing your handoff code. The provider returns to AnyOAuth's provider callback, where the service checks the transaction's state and initiating-browser binding. Google identity tokens are validated; GitHub identity is retrieved through fresh profile/email lookup. Provider tokens aren't persisted or delivered to your backend.

After that leg succeeds, the browser returns to your registered application callback with an AnyOAuth handoff code and your original client state. That code is not Google's or GitHub's authorization code. It lasts two minutes and can be redeemed only once. Your user is not yet signed into your application.

Users see the service's provider identity during upstream consent. This intermediate service is a real part of the flow, not a transparent alias for your own provider registration.

## 3. Consume and validate, then exchange on the backend

Your callback handler must load and atomically consume the saved transaction, including when the user denied sign-in. Validate state against the initiating browser session and the incoming callback against the saved registered callback. An expired, absent, or already consumed transaction must stop here. Don't take a fresh callback URL from visitor input.

<figure class="article-diagram"><ol><li><strong>Callback arrives:</strong> the backend finds the initiating browser’s saved transaction, consumes it once, and validates state, expiry, and callback.</li><li><strong>Exchange result is uncertain:</strong> a network interruption may occur after AnyOAuth consumed the handoff code. Replaying the same code is not a safe retry strategy.</li><li><strong>Recovery:</strong> offer a new sign-in transaction. Do not issue a session from the callback alone or reuse the old verifier/state pair.</li></ol><figcaption>A single-use exchange changes failure recovery. A missing response does not prove the server left the code unused.</figcaption></figure>

For a successful callback, the backend posts to `/v1/token` with the project client ID and secret, handoff code, original verifier, and saved callback. Client authentication and S256 proof are both required. The SDK never automatically retries this exchange. A denial isn't a code to redeem; provide a retry action without creating a logged-in session.

This is where a general-purpose HTTP retry wrapper deserves scrutiny. Its normal behavior may conflict with a one-time credential. Review the exchange call specifically rather than assuming every backend POST shares the same retry policy.

## 4. Resolve the profile into your own user

The exchange returns an opaque AnyOAuth profile token, not a provider access token or an ID token for local JWT decoding. Use it as a bearer credential at `/v1/profile`. Its lifetime is 15 minutes, with no refresh flow, and it provides no provider API access.

Find or create your application user using the stable `profile.subject`. That subject is scoped to project and provider account. Treat names, usernames, email, and avatars as nullable display/contact data, not identity keys. Matching emails must not automatically merge accounts. The [profile-token note](/blog/a-profile-token-not-a-provider-token/) explains why this credential isn't your session either.

Issue the application's own secure session cookie only after identity lookup and your login policy succeed. Revoke the profile token when finished, then redirect to a clean application URL without code/state. App logout and permission checks remain app-owned. Expiring or revoking the profile token doesn't invalidate an already-created app session.

## AnyOAuth implementation choice

AnyOAuth, which we build, is useful when this explicit profile handoff matches your backend and configured Google/GitHub requirements. It is not a customer-facing OIDC/SAML server or a provider API proxy. A generic OIDC adapter expecting discovery and an ID token won't match this contract.

For the DIY path, use maintained provider libraries with your own registrations, implement both identity checks, and join them at your user/session boundary. That is preferable if your app needs its own provider consent identity or provider API scopes. Either approach leaves privacy and downstream data use with your application; neither automatically establishes legal compliance.

Before shipping, trace a denial, wrong state, expired transaction, reused code, nullable profile, and rejected return destination. Use [secure handoffs](/blog/secure-oauth-handoffs/) for the rejection reasoning rather than treating the happy path as the complete integration.

## Sources

Read or retrieved 2026-10-06:

- AnyOAuth, [Social sign-in quickstart](/docs/), [Security model](/security/), and [Privacy & compliance considerations](/compliance/).
- IETF, [RFC 7636: Proof Key for Code Exchange, §4](https://www.rfc-editor.org/rfc/rfc7636.html#section-4).
- IETF, [RFC 9700: OAuth security BCP, §2.1](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1), on redirect and code-flow protections.