{"record":{"id":"eb212a708a7f72de","repo":"toeverything/AFFiNE","slug":"invalid-auth-state-eb212a","errorCode":"invalid_auth_state","errorMessage":"Invalid auth state. You might start the auth progress from another device.","messagePattern":"Invalid auth state\\. You might start the auth progress from another device\\.","errorType":"exception","errorClass":"InvalidAuthState","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/service.ts","lineNumber":134,"sourceCode":"    }\n\n    if (!state.provider) {\n      throw new MissingOauthQueryParameter({ name: 'provider' });\n    }\n\n    const provider = this.providerFactory.get(state.provider);\n\n    if (!provider) {\n      throw new UnknownOauthProvider({ name: state.provider ?? 'unknown' });\n    }\n\n    if (\n      state.provider !== OAuthProviderName.Apple &&\n      (!input.clientNonce ||\n        !state.clientNonce ||\n        state.clientNonce !== input.clientNonce)\n    ) {\n      throw new InvalidAuthState();\n    }\n\n    return {\n      type: 'identity',\n      identity: await this.verifyCallbackIdentity(\n        input.code,\n        state,\n        stateStr,\n        input.rawBody\n      ),\n      state,\n    };\n  }\n\n  async verifyCallbackIdentity(\n    code: string,\n    state: OAuthState,\n    stateStr: string,","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/oauth/service.ts#L116-L152","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst pre = await fetch('/api/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'oidc', client: 'web' }) });\nawait fetch('/api/oauth/callback', { method: 'POST', body: JSON.stringify({ code, state }) }); // nonce lost\n\n// after\nconst clientNonce = crypto.randomUUID();\nconst pre = await fetch('/api/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'oidc', client: 'web', client_nonce: clientNonce }) });\n// ...redirect round-trip...\nawait fetch('/api/oauth/callback', { method: 'POST', body: JSON.stringify({ code, state, client_nonce: clientNonce }) });","handlingStrategy":"validation","validationCode":"// Client: persist the nonce and assert before the callback\nconst clientNonce = crypto.randomUUID();\nsessionStorage.setItem('oauth_client_nonce', clientNonce);\n// ...after IdP redirect...\nconst nonce = sessionStorage.getItem('oauth_client_nonce');\nif (!nonce) throw new Error('Login session lost - restart sign-in');\nawait postCallback({ code, state, client_nonce: nonce });","typeGuard":null,"tryCatchPattern":"try {\n  await oauth.verifyCallback({ code, stateStr, clientNonce });\n} catch (err) {\n  if (err instanceof InvalidAuthState) {\n    // nonce mismatch: clear stored nonce and restart the flow from preflight\n  }\n}","preventionTips":["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."],"tags":["oauth","csrf","nonce","callback","authentication"],"backgroundTag":"oauth-nonce-mismatch","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}