toeverything/AFFiNE · error · InvalidAuthState
invalid_auth_state
invalid_auth_state
Error message
Invalid auth state. You might start the auth progress from another device.
What it means
InvalidAuthState thrown at packages/backend/server/src/plugins/oauth/service.ts:134 when the client_nonce posted to /api/oauth/callback does not match the clientNonce stored in the state at preflight time (non-Apple providers only). The nonce is an anti-CSRF/anti-cross-device binding: the browser that started the flow must be the one that finishes it, and Apple flows are exempted because of their form_post handoff.
Source
Thrown at packages/backend/server/src/plugins/oauth/service.ts:134
}
if (!state.provider) {
throw new MissingOauthQueryParameter({ name: 'provider' });
}
const provider = this.providerFactory.get(state.provider);
if (!provider) {
throw new UnknownOauthProvider({ name: state.provider ?? 'unknown' });
}
if (
state.provider !== OAuthProviderName.Apple &&
(!input.clientNonce ||
!state.clientNonce ||
state.clientNonce !== input.clientNonce)
) {
throw new InvalidAuthState();
}
return {
type: 'identity',
identity: await this.verifyCallbackIdentity(
input.code,
state,
stateStr,
input.rawBody
),
state,
};
}
async verifyCallbackIdentity(
code: string,
state: OAuthState,
stateStr: string,View on GitHub (pinned to b4c8548c09)
Solutions
- Send the exact same client_nonce in the /api/oauth/callback body that you generated and sent to /api/oauth/preflight.
- Keep the nonce in the same browser storage (sessionStorage) for the whole redirect round-trip.
- Do not start preflight on one device and complete the callback on another — restart the flow instead.
- For native clients, persist the nonce alongside the state envelope until the callback completes.
Example fix
// before
const pre = await fetch('/api/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'oidc', client: 'web' }) });
await fetch('/api/oauth/callback', { method: 'POST', body: JSON.stringify({ code, state }) }); // nonce lost
// after
const clientNonce = crypto.randomUUID();
const pre = await fetch('/api/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'oidc', client: 'web', client_nonce: clientNonce }) });
// ...redirect round-trip...
await fetch('/api/oauth/callback', { method: 'POST', body: JSON.stringify({ code, state, client_nonce: clientNonce }) }); Defensive patterns
Strategy: validation
Validate before calling
// Client: persist the nonce and assert before the callback
const clientNonce = crypto.randomUUID();
sessionStorage.setItem('oauth_client_nonce', clientNonce);
// ...after IdP redirect...
const nonce = sessionStorage.getItem('oauth_client_nonce');
if (!nonce) throw new Error('Login session lost - restart sign-in');
await postCallback({ code, state, client_nonce: nonce }); Try / catch
try {
await oauth.verifyCallback({ code, stateStr, clientNonce });
} catch (err) {
if (err instanceof InvalidAuthState) {
// nonce mismatch: clear stored nonce and restart the flow from preflight
}
} Prevention
- Reuse the identical client_nonce for preflight and callback.
- Store it in sessionStorage for the duration of the redirect round-trip.
- Never share a login attempt across devices; restart instead.
- Apple flows are exempt — do not add workaround logic for them.
When it happens
Trigger: Callback request omits client_nonce; the client generates a fresh nonce for the callback instead of reusing the one from preflight; the flow was started in browser/session A and finished in browser/session B (different preflight nonces); a hand-rolled client that does not persist the nonce between the two calls.
Common situations: Custom client integrations and CLI scripts that skip client_nonce; service workers or redirects losing the nonce; users copying the callback URL into another browser; load-balanced web clients that store the nonce in a non-shared session store.
Related errors
- invalid_oauth_callback_state
- oauth_state_expired
- user_not_found
- authentication_required
- invalid_auth_state
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/eb212a708a7f72de.
Report an issue: GitHub.