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
- Generate a fresh sign-in code via createSignInCode and complete verification within 5 minutes.
- Ensure verify is called exactly once per code; request a new code on any failure.
- Confirm the challenge store backend is running and not being cleared mid-flow.
- 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
- Consume the open-app sign-in code within 5 minutes of creation.
- Use each code exactly once; regenerate on any failure.
- Ensure the challenge store (cache/redis) is healthy and not flushed mid-flow.
- Pass the code verbatim from createSignInCode without truncation.
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.