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.

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@d897bb3d84 (2026-08-18). Data as JSON: /api/errors/9086412e5b5f9352. Report an issue: GitHub.

Appendix: source

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

    redirectUri?: string;
    client: string;
    clientNonce: string;
    clientVersion?: string;
    callbackUrl: string;
    redirectBaseUrl: string;
    redirectAllowedOrigins: string[];
    redirectTrustedDomains: string[];
  }) {
    return await this.call<{ url: string }>({
      action: 'oauth_preflight',
      ...input,
    });
  }

  async callback(input: {
    code: string;
    state: string;
    clientNonce?: string;
    issue: SessionIssueInput;
  }) {
    return await this.call<NativeOAuthCallback>({
      action: 'oauth_callback',
      ...input,
    });
  }

  private async call<T>(input: Record<string, unknown>) {
    try {
      return await this.runtime.executeAuthSessionCommandV1<T>(input);
    } catch (error) {
      const message = String(error);
      if (message.includes('unknown_oauth_provider')) {
        throw new UnknownOauthProvider({ name: String(input.provider ?? '') });
      }
      if (message.includes('invalid_oauth_callback_state')) {
        throw new InvalidOauthCallbackState();

View on GitHub (pinned to d897bb3d84)