toeverything/AFFiNE · error · OauthStateExpired

oauth_state_expired

oauth_state_expired

Error message

OAuth state expired, please try again.

What it means

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.

Source

Thrown at packages/backend/server/src/plugins/oauth/service.ts:100

  }): Promise<VerifyCallbackResult> {
    let stateStr = input.stateStr;
    let rawState: { state: string; provider?: string } | null = null;
    if (typeof stateStr === 'string' && stateStr.length > 36) {
      try {
        const parsed = OAuthStateEnvelopeSchema.safeParse(JSON.parse(stateStr));
        if (parsed.success) {
          rawState = parsed.data;
          stateStr = rawState.state;
        }
      } catch {} // noop
    }

    if (typeof stateStr !== 'string' || !this.isValidState(stateStr)) {
      throw new InvalidOauthCallbackState();
    }

    const state = await this.getOAuthState(stateStr);
    if (!state) throw new OauthStateExpired();
    if (!state.token) state.token = stateStr;

    if (
      state.provider === OAuthProviderName.Apple &&
      rawState &&
      state.client &&
      state.client !== 'web'
    ) {
      return {
        type: 'handoff',
        code: input.code,
        provider: rawState.provider,
        state,
        stateToken: stateStr,
      };
    }

    if (!state.provider) {

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Have the user restart the sign-in flow — a fresh preflight issues a new state.
  2. For multi-instance deployments, point all nodes at the same shared Redis session cache so states are visible cluster-wide.
  3. If states expire too fast in your environment, ensure the cache backend is persistent (Redis AOF/RDB) rather than in-memory.
  4. Check for clock/TTL misconfiguration or Redis maxmemory eviction policy that is evicting oauth_state keys.
Defensive patterns

Strategy: retry

Try / catch

try {
  await oauth.verifyCallback(input);
} catch (err) {
  if (err instanceof OauthStateExpired) {
    // user-facing: 'session expired, please sign in again' -> re-run preflight
  }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/9086412e5b5f9352. Report an issue: GitHub.