OAuth · beginner

OAuth callback checklist

A practical OAuth callback checklist for engineers diagnosing cases where the authorization callback fails to restore the initiating session. Walks through ordered triage of redirect URI matching, state parameter binding, cookie attributes, and PKCE binding before any code change, so the fix is anchored to evidence rather than guesswork.

The symptoms

  • After consent at the authorization server, the user lands on the client redirect URI but is treated as unauthenticated or routed back to the original login prompt.
  • The callback URL contains code and state, but the client application cannot correlate the response with the original session that initiated the flow.
  • Authentication succeeds intermittently, breaking specifically when the callback crosses a SameSite boundary, an iframe boundary, or a sub-domain boundary.
  • Browser DevTools show the authorization response, but the session cookie that existed before redirect is absent or marked HttpOnly without a corresponding value on subsequent requests.

Likely causes

  • Mismatch between the registered redirect URI at the authorization server and the exact URI used in the authorize request, including scheme, host, port, and trailing path differences.
  • State parameter not generated, not stored against the originating session, not round-tripped, or checked against the wrong session identifier after the callback.
  • Session cookie lost across the redirect because of SameSite, Secure, Domain, Path, or cross-site request semantics that the developer did not account for.
  • PKCE code verifier not bound to the originating session or not available when the client exchanges the authorization code for tokens.
  • Multiple tabs, concurrent flows, or session rotation between authorize and callback causing the original session to be replaced before the state check runs.

First ten minutes

  1. 01Reproduce the flow once with the browser DevTools Network panel open, filtered to the authorize endpoint and the redirect URI, and capture the full request and response headers plus the query string.
  2. 02Compare the exact redirect_uri value in the authorize request against the registered redirect URI at the authorization server, treating trailing slashes, query parameters, and fragment differences as distinct.
  3. 03Inspect the Set-Cookie attributes set by the client domain before redirect and after callback, noting SameSite, Secure, Domain, Path, Expires, and HttpOnly for each.
  4. 04Confirm the state parameter sent in the authorize request matches the value the client stored, and that the stored value was bound to the pre-redirect session identifier rather than to a new one.
  5. 05Open the Application Storage panel and record which cookies exist on the client domain before authorize, which survive the redirect, and which are recreated after callback.

Evidence to collect

  • Exact string of the redirect_uri query parameter in the authorize request and the canonical registered value at the authorization server.
  • Full Set-Cookie header lines for every cookie set by the client domain during the flow, including attributes defined in RFC 6265 bis as documented by MDN.
  • The state value present in the authorize request and in the callback query string, plus the session identifier it was bound to in client storage.
  • Browser console messages, network waterfall timings, and any client-side errors raised when the callback handler attempts to validate state and exchange the code.
  • Authorization server logs or error responses that describe the redirect URI mismatch, invalid state, or token exchange failure for the specific request.

Where to look

  • Browser DevTools Network panel at the boundary between the authorization server and the client redirect URI, where the cross-site redirect strips or preserves cookies.
  • Application Storage and Cookies panel scoped to the client application domain, before and after the redirect, to observe cookie scope changes.
  • Authorization server configuration console or admin API, where the registered redirect URIs, allowed scopes, and client authentication method are defined.
  • Server-side session store or signed cookie store, where the binding between the original session id and the state value must be persisted across the redirect.
  • Client callback handler source, specifically the order in which state is validated, code is exchanged, and the session cookie is regenerated.

Diagnostic steps

  1. 01Compare registered redirect URI to the exact authorize request URI byte-for-byte; any difference in scheme, host, port, path, or trailing slash invalidates the callback and must be corrected before other theories are pursued.
  2. 02Verify the state parameter is cryptographically unguessable, stored server-side against the originating session, and read back from the callback against that same session record rather than from a freshly created one.
  3. 03Inspect Set-Cookie attributes against the cross-site redirect boundary and confirm the session cookie will survive, adjusting SameSite and Secure per the MDN Set-Cookie reference rather than guessing.
  4. 04Confirm the PKCE code verifier was stored against the originating session before redirect and is available at code exchange; a verifier recreated after callback defeats PKCE.
  5. 05Test the flow in a single browser tab with no other OAuth tabs open, to rule out session rotation caused by parallel flows before declaring a configuration defect.
  6. 06Review the callback handler for any operation that issues a new session identifier before the original state check, which silently invalidates the binding the checklist exists to protect.

Common mistakes

  • Treating redirect URI mismatch as a deployment typo and editing the registered value at the authorization server without first confirming which exact URI the client sent, which often reveals an environment or proxy rewrite issue instead.
  • Validating the state parameter against a cookie value rather than against a server-side session record, which makes the check useless the moment the cookie is cleared by the browser.
  • Loosening SameSite on the session cookie to mask a lost binding, which papers over the real defect and can introduce cross-site request forgery exposure without restoring the original session.
  • Concluding that the authorization server is broken because the callback fires, when in fact the client callback handler is reading query parameters from a wrong or duplicated route.
  • Assuming a successful token exchange implies the original session was restored, when the token exchange can succeed while the client still authenticates the callback response against a new session identifier.

Safe fixes

  • If redirect URIs differ, align the client configuration and the authorization server registration to the exact URI the authorize request actually sends, then re-run the flow in a clean browser profile to confirm.
  • If state validation fails, persist the state value server-side against the originating session id before redirect, read it back at callback time using the current session id, and only then exchange the code.
  • If the session cookie is dropped across the redirect, set its SameSite and Secure attributes according to the MDN Set-Cookie reference and confirm the cookie scope matches the callback host before changing application code.
  • If PKCE binding is lost, store the code verifier against the originating session before redirect and clear it only after a successful token exchange, so a second callback cannot replay it.
  • If session rotation is the cause, ensure the callback handler reads state and exchanges the code before any operation that issues a new session identifier, so the original binding remains valid.

Prove the fix

  1. 01In a clean browser profile, run the full authorize to callback flow once and observe in DevTools that the session cookie set before redirect is the same one present after callback, with identical value and scope.
  2. 02Confirm the authorization server accepts the redirect URI without error and the callback query string contains both code and state, with state matching the server-side record bound to the originating session.
  3. 03Verify a second identical authorize request fails the state check because the stored binding was consumed or rotated, proving the binding is single-use and tied to the original session.
  4. 04Inspect the token exchange response and the post-callback session record to confirm the user identity is associated with the session id that existed before the redirect, not a new one.
  5. 05Repeat the flow across at least one boundary where it previously failed (sub-domain, cross-site, or iframe) and confirm the session survives end-to-end using the same cookie value.

Prevention and next steps

  • Maintain a single source of truth for redirect URIs, generated from the client configuration and registered at the authorization server through a documented review step, so registration drift cannot reintroduce the mismatch.
  • Persist OAuth flow state and PKCE verifier server-side against the session id, and treat any callback whose session id has no binding as a hard failure rather than a retry.
  • Add an automated test that performs the full authorize to callback flow in a clean browser context and asserts that the session cookie value is unchanged across the redirect, catching regressions before production.
  • Review Set-Cookie attributes for the session cookie whenever the deployment topology or cookie domain changes, using the MDN Set-Cookie reference rather than relying on browser defaults.

Safe commands and checks

grep -n 'redirect_uri' <client-config-file>
grep -n 'state' <callback-handler-source>
grep -n 'code_verifier' <client-oauth-module>
openssl rand -base64 32
tail -n 200 <authorization-server-log-path>