{"record":{"id":"43651caa264256e8","repo":"toeverything/AFFiNE","slug":"invalid-auth-state-43651c","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/providers/oidc.ts","lineNumber":285,"sourceCode":"        'claim_email_verified'\n      ),\n      state,\n      nonce,\n    };\n\n    if (pkce) {\n      query.code_challenge = pkce.codeChallenge;\n      query.code_challenge_method = pkce.codeChallengeMethod;\n    }\n\n    return `${this.endpoints.authorization_endpoint}?${this.url.stringify(\n      query\n    )}`;\n  }\n\n  async getToken(code: string, state: OAuthState): Promise<Tokens> {\n    if (this.requiresPkce && !state.pkce?.codeVerifier) {\n      throw new InvalidAuthState();\n    }\n\n    const data = await this.postFormJson<unknown>(\n      this.endpoints.token_endpoint,\n      this.url.stringify({\n        code,\n        client_id: this.config.clientId,\n        client_secret: this.config.clientSecret,\n        redirect_uri: this.url.link('/oauth/callback'),\n        grant_type: 'authorization_code',\n        ...(state.pkce?.codeVerifier\n          ? { code_verifier: state.pkce.codeVerifier }\n          : {}),\n      }),\n      { treatServerErrorAsInvalid: true }\n    );\n\n    const tokens = OIDCTokenSchema.parse(data);","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/oauth/providers/oidc.ts#L267-L303","documentation":"OIDCProvider.getToken exchanges the authorization code at the IdP token endpoint. If this provider requires PKCE but the stored OAuthState lacks state.pkce.codeVerifier, it throws InvalidAuthState before making the request — the verifier is needed to prove the code belongs to this flow, and without it the exchange cannot proceed. The verifier is created together with the challenge during preflight; a state record with a challenge but no verifier is inconsistent.","triggerScenarios":"Callback arrives with a state whose server-side record was written without a code verifier: state saved by an older server version, a state envelope from a different flow type (challenge present, verifier dropped), or session/state mismatch where the callback's state maps to a record from another device or an incomplete preflight.","commonSituations":"Server upgrade while logins were in flight; user completing the callback in a different browser/device than the one that started preflight; multiple pending logins with mixed versions; 'start the auth progress from another device' is literally this case.","solutions":["Restart the login flow end-to-end in one browser session: fresh preflight → authorize → callback","Complete the callback on the same device/browser that initiated the preflight — the verifier lives in that flow's server-side state","After server upgrades, expect in-flight OAuth sessions to fail; have users sign in again rather than resuming"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// ensure the callback completes against the same preflight that started the flow\nconst pending = sessionStorage.getItem('affine_oauth_state');\nif (pending !== stateStr) {\n  throw new Error('Callback state does not match the pending login — restart the flow on one device');\n}","typeGuard":"function isInvalidAuthState(e: unknown): boolean {\n  return typeof e === 'object' && e !== null && (e as any).code === 'invalid_auth_state';\n}","tryCatchPattern":"try {\n  await post('/oauth/callback', { code, state });\n} catch (e) {\n  if (isInvalidAuthState(e)) {\n    clearPendingOAuthSession();\n    return restartFromPreflight(); // verifier lives in the original flow's state — cannot be recovered\n  }\n  throw e;\n}","preventionTips":["Complete the whole OAuth flow on the device and browser that started it","Store which preflight issued the pending state and abort early on mismatch instead of exchanging the code","During server upgrades, drain in-flight OAuth sessions or expect InvalidAuthState callbacks from stale states"],"tags":["oauth","oidc","pkce","code-verifier","auth-state"],"backgroundTag":"oauth-pkce-missing","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}