{"record":{"id":"9086412e5b5f9352","repo":"toeverything/AFFiNE","slug":"oauth-state-expired-908641","errorCode":"oauth_state_expired","errorMessage":"OAuth state expired, please try again.","messagePattern":"OAuth state expired, please try again\\.","errorType":"exception","errorClass":"OauthStateExpired","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/service.ts","lineNumber":100,"sourceCode":"  }): Promise<VerifyCallbackResult> {\n    let stateStr = input.stateStr;\n    let rawState: { state: string; provider?: string } | null = null;\n    if (typeof stateStr === 'string' && stateStr.length > 36) {\n      try {\n        const parsed = OAuthStateEnvelopeSchema.safeParse(JSON.parse(stateStr));\n        if (parsed.success) {\n          rawState = parsed.data;\n          stateStr = rawState.state;\n        }\n      } catch {} // noop\n    }\n\n    if (typeof stateStr !== 'string' || !this.isValidState(stateStr)) {\n      throw new InvalidOauthCallbackState();\n    }\n\n    const state = await this.getOAuthState(stateStr);\n    if (!state) throw new OauthStateExpired();\n    if (!state.token) state.token = stateStr;\n\n    if (\n      state.provider === OAuthProviderName.Apple &&\n      rawState &&\n      state.client &&\n      state.client !== 'web'\n    ) {\n      return {\n        type: 'handoff',\n        code: input.code,\n        provider: rawState.provider,\n        state,\n        stateToken: stateStr,\n      };\n    }\n\n    if (!state.provider) {","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/oauth/service.ts#L82-L118","documentation":"OauthStateExpired thrown at packages/backend/server/src/plugins/oauth/service.ts:100 when the 36-char state UUID is well-formed but not found in the challenge store (AuthChallengeStore over SessionCache with a 3-hour TTL, see OAUTH_STATE_TTL_MS). The state was never issued, has already expired, was consumed, or lives on another server node's cache.","triggerScenarios":"Completing the OAuth callback more than 3 hours after preflight; server restart that wiped an in-memory/Redis cache without persistence; multi-instance deployment where the callback hits a node that does not share the session cache; the user's browser replaying an old callback; a state UUID that was fabricated (valid format, never saved).","commonSituations":"Self-hoster running several backend replicas behind a load balancer with cache pointing at different Redis DBs or memory cache; Redis flushed/evicted; user left the login tab open overnight; Docker restart with AFFINE default in-memory cache in production.","solutions":["Have the user restart the sign-in flow — a fresh preflight issues a new state.","For multi-instance deployments, point all nodes at the same shared Redis session cache so states are visible cluster-wide.","If states expire too fast in your environment, ensure the cache backend is persistent (Redis AOF/RDB) rather than in-memory.","Check for clock/TTL misconfiguration or Redis maxmemory eviction policy that is evicting oauth_state keys."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await oauth.verifyCallback(input);\n} catch (err) {\n  if (err instanceof OauthStateExpired) {\n    // user-facing: 'session expired, please sign in again' -> re-run preflight\n  }\n}","preventionTips":["Share one Redis session cache across all backend replicas.","Use a persistent Redis (AOF/RDB) so restarts do not wipe oauth_state keys.","Complete login promptly; states live 3 hours.","Avoid Redis eviction policies that evict auth_challenge:* keys under memory pressure."],"tags":["oauth","state-expired","cache","distributed-systems","authentication"],"backgroundTag":"oauth-state-expired","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}