toeverything/AFFiNE · error · InvalidAuthState

invalid_auth_state

invalid_auth_state

Error message

Invalid auth state. You might start the auth progress from another device.

What it means

OpenAppAuthService.verifySignInCode consumes the 'open_app_sign_in' challenge (a short-lived 5-minute single-use code created by createSignInCode). If consume returns null/undefined or a payload without a userId, InvalidAuthState is thrown. This means the code is missing, expired, already consumed, or was malformed — there is no valid user identity to authenticate.

Source

Thrown at packages/backend/server/src/core/auth/open-app.ts:27

export class OpenAppAuthService {
  constructor(private readonly challenges: AuthChallengeStore) {}

  async createSignInCode(user: CurrentUser) {
    return this.challenges.create(
      'open_app_sign_in',
      { userId: user.id },
      5 * 60 * 1000
    );
  }

  async verifySignInCode(code: string): Promise<VerifiedIdentity> {
    const payload = await this.challenges.consume<{ userId?: string }>(
      'open_app_sign_in',
      code
    );

    if (!payload?.userId) {
      throw new InvalidAuthState();
    }

    return { userId: payload.userId, method: 'open_app' };
  }
}

View on GitHub (pinned to 26c515e050)

Solutions

  1. Generate a fresh sign-in code via createSignInCode and complete verification within 5 minutes.
  2. Ensure verify is called exactly once per code; request a new code on any failure.
  3. Confirm the challenge store backend is running and not being cleared mid-flow.
  4. Pass the code value verbatim from createSignInCode without truncation.
Defensive patterns

Strategy: try-catch

Type guard

function isInvalidAuthState(err: unknown): boolean {
  return (
    !!err &&
    typeof err === 'object' &&
    (err as { code?: string }).code === 'invalid_auth_state'
  );
}

Try / catch

try {
  await openApp.verifySignInCode(code);
} catch (err) {
  if (isInvalidAuthState(err)) {
    code = await openApp.createSignInCode(user); // refresh
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: verifySignInCode(code) called with a code that does not exist, was already consumed (one-time use), or is past its 5-minute TTL — so challenges.consume returns no payload or a payload lacking userId.

Common situations: User let the QR/deep-link sign-in code expire before confirming. The same code was submitted twice (double-tap). The challenge store (Redis/cache) was flushed, dropping the code. A code generated for a different purpose was passed.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/d9fe744f3cfc7808. Report an issue: GitHub.