Protect an OAuth callback from mismatched state

An OAuth callback must belong to the browser flow that initiated it. A random state value bound to that session gives the application something to compare before it exchanges an authorization code.
A callback can proceed to code exchange only after state and expiry match the browser-bound flow.
Bind a one-time value to the flow
This local example demonstrates state generation and consumption with a synthetic session dictionary. It does not contact an authorization server.
import secrets
import time
def begin_flow(session):
state = secrets.token_urlsafe(32)
session["oauth_flow"] = {
"state": state,
"expires_at": time.time() + 600,
}
return state
def consume_callback(session, returned_state):
flow = session.pop("oauth_flow", None)
if flow is None or time.time() > flow["expires_at"]:
raise ValueError("Missing or expired authorization flow")
if not isinstance(returned_state, str):
raise ValueError("Missing state")
if not secrets.compare_digest(flow["state"], returned_state):
raise ValueError("Authorization state mismatch")
return True
The ten-minute expiry is an illustrative application choice. The state goes into the authorization request; the server-side session retains the expected value. The callback compares them before performing any code exchange.
Popping the flow makes this simple version one-time. It also means a mismatched callback consumes the pending flow, and starting a second flow replaces the first. A product supporting several concurrent connection tabs needs a bounded store of independent flow records.
State is one part of the protocol
Use the provider's current authorization-code guidance, including PKCE where required or supported for the client type. Validate redirect URIs and issuer assumptions, and ensure the callback cannot be redirected to an arbitrary URL supplied by the browser.
Session binding matters. A random value stored only in a client-controlled parameter does not establish which logged-in account should receive the resulting connection. Persist the intended account and scope with the flow and recheck the initiating user's permission when the callback returns.
Test a successful callback, wrong state, missing state, expiration and reuse. Confirm a rejected callback performs no token exchange and writes no account connection.
Authorization codes and access tokens should stay out of routine logs. The state value can also be sensitive flow material, so avoid including callback query strings in unrestricted analytics.
Postzyo provides related OAuth account-connection context. RFC 9700 covers OAuth security, and Python documents secure random tokens.