Two values your app must remember across the redirect
A sign-in redirect leaves your site and comes back, so the app has to remember something about the attempt that started it. The state value is a random token sent out with the request and returned unchanged with the code. RFC 6749 describes it as an opaque value used to maintain state between the request and the callback, and current security guidance (RFC 9700) says clients must prevent cross-site request forgery, either with a one-time value bound to the user agent or with PKCE. PKCE adds a second secret: the app creates a code verifier for each attempt, sends only a hash of it (the code challenge), and must present the original verifier when it trades the code for tokens.
If either remembered value is missing when the user returns, a correct app must refuse the callback. A failed return is therefore often the security check working, with the real fault being that the value was not stored or not found.
- State proves the return belongs to a sign-in this browser started.
- The verifier proves the app that asks for tokens is the app that started the attempt.
- Neither check should be switched off to make a sign-in pass.
How the remembered value gets lost
The value is usually kept in a cookie or a server-side session keyed by a cookie. Three common faults remove it. First, the host changes between start and return, for example the sign-in starts on the bare domain and returns to the www host, so the browser sends a host-only cookie to the wrong place. Second, the cookie attributes make the browser withhold it: SameSite=Lax cookies are sent on cross-site top-level navigations that use a safe method, but not on a cross-site POST, so a return delivered as a POST may arrive without them. Third, the person opened sign-in in two tabs, or the page was reloaded, and a later attempt replaced the stored value belonging to the earlier one.
The verifier has a fourth failure: it is generated again on the return step instead of being read back, so the token request carries a different verifier from the challenge that was sent. RFC 7636 requires the server to answer a mismatch with invalid_grant.
- Compare the host at the start with the host at the return, character for character.
- Check the attributes of the cookie that holds the value (see the cookie guide).
- Check that each attempt stores its own value, so two tabs do not overwrite each other.
A code can be used only once
The authorization code returned to your callback is short-lived and single-use. RFC 6749 requires it to expire shortly after issue, recommends a maximum of 10 minutes, and forbids a client from using it more than once. A callback handler that runs twice for one return, for example because of a duplicated request, a page prefetch, a double click or a development setting that renders an effect twice, spends the code on the first run and fails on the second. The user sees an error although the first exchange may have succeeded and been discarded.
- Count how many times your callback route is hit for one sign-in.
- Make the exchange idempotent: record that this state value was consumed.
- Do not retry a failed exchange with the same code.
A safe first investigation
Use a test account. At the moment your callback runs, log only booleans, never values: whether a stored state exists, whether it equals the returned one, whether a verifier exists and how many times the route was called. Compare the host and path at the start with those at the return. Do not log tokens, codes or cookie values, and do not paste them into a support request.
- If the stored state is missing, look at the host and the cookie attributes first.
- If the stored state is present but different, look for overlapping attempts.
- If state matches and the exchange still fails, look at the redirect address sent in both requests and at the verifier.
What fixes it and how the paid job is accepted
A repair stores one state and one verifier per attempt in a place the return can reach, reads them back instead of regenerating them, and makes the exchange run once. It does not loosen the state check. In our fixed-price sign-in repair (posted test price £195, untested), acceptance includes a test that a return with a missing or altered state value is rejected and creates no session, as well as a successful test sign-in in each agreed environment. The job does not cover a provider-side review, single sign-on for business customers or merging duplicate accounts. Send the provider and the redacted error in your first enquiry, not codes, tokens or secrets.
- If the session cookie is set and still dropped after a successful return, that is the login-session job instead.
Sources and limits
- RFC 6749: The OAuth 2.0 Authorization Framework Checked 2026-10-11.
- The state parameter is recommended and is returned by the server; the authorization code must expire shortly (a maximum of 10 minutes is recommended) and must not be used more than once (section 4.1.2).
- RFC 9700: OAuth 2.0 Security Best Current Practice (January 2025) Checked 2026-10-11.
- Clients must prevent cross-site request forgery, using a one-time value bound to the user agent in state, or PKCE where the server is confirmed to support it (section 2.1).
- RFC 7636: Proof Key for Code Exchange Checked 2026-10-11.
- The client creates a code_verifier per authorization request, sends a derived code_challenge, and must send the verifier with the token request; a mismatch returns invalid_grant; the verifier is 43 to 128 characters (sections 4.1 to 4.6).
- MDN: Set-Cookie header Checked 2026-10-11.
- SameSite=Lax cookies are sent on cross-site top-level navigations with safe methods, not on cross-site POST requests.