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
During MagicLinkAuthService.verify, magicLinkOtp.consume returns ok:false with reason 'nonce_mismatch', meaning the OTP is valid for the email but the clientNonce provided at verify time differs from the nonce stored when the OTP was created (upserted during send). The server reports InvalidAuthState ('You might start the auth progress from another device') rather than revealing the OTP was wrong. This binds OTP consumption to the originating client to prevent OTP replay from a different session.
Source
Thrown at packages/backend/server/src/core/auth/magic-link.ts:111
return { email };
}
async verify(
email: string,
otp: string,
clientNonce?: string
): Promise<VerifiedIdentity> {
validators.assertValidEmail(email);
const consumed = await this.models.magicLinkOtp.consume(
email,
otp,
clientNonce
);
if (!consumed.ok) {
if (consumed.reason === 'nonce_mismatch') {
throw new InvalidAuthState();
}
throw new InvalidEmailToken();
}
const tokenRecord = await this.models.verificationToken.verify(
TokenType.SignIn,
consumed.token,
{
credential: email,
}
);
if (!tokenRecord) {
throw new InvalidEmailToken();
}
const user = await this.models.user.fulfill(email);
View on GitHub (pinned to 26c515e050)
Solutions
- Ensure the same clientNonce is captured at send time and replayed at verify time (persist it in sessionStorage/localStorage keyed by email).
- Complete the verification in the same browser tab/device that initiated the send.
- If state was lost, re-trigger send to obtain a fresh OTP+nonce pair before verifying.
- Pass clientNonce consistently from the client through both API calls.
Example fix
// before: nonce lost between calls
await send(email);
// ... page reload ...
await verify(email, otp); // missing nonce -> invalid_auth_state
// after: persist and replay
const nonce = crypto.randomUUID();
sessionStorage.setItem(`magic-nonce:${email}`, nonce);
await send(email, '/magic-link', nonce);
// later:
const nonce = sessionStorage.getItem(`magic-nonce:${email}`) ?? undefined;
await verify(email, otp, nonce); Defensive patterns
Strategy: validation
Validate before calling
const nonce = crypto.randomUUID();
sessionStorage.setItem(`magic-nonce:${email}`, nonce);
await send(email, '/magic-link', nonce);
// later, in the same device/session:
const storedNonce = sessionStorage.getItem(`magic-nonce:${email}`);
if (!storedNonce) throw new Error('nonce lost — restart flow');
await verify(email, otp, storedNonce); Type guard
function isInvalidAuthState(err: unknown): boolean {
return (
!!err &&
typeof err === 'object' &&
(err as { code?: string }).code === 'invalid_auth_state'
);
} Try / catch
try {
await verify(email, otp, nonce);
} catch (err) {
if (isInvalidAuthState(err)) {
showHint('Use the same device that requested the code, then retry.');
await restartMagicLinkFlow(email);
return;
}
throw err;
} Prevention
- Persist the clientNonce in sessionStorage keyed by email across the send->verify round trip.
- Complete verification on the same device/tab that initiated send.
- Generate a fresh nonce per send and discard after successful verify.
- On any page reload between send and verify, re-initiate the flow.
When it happens
Trigger: The magic-link send was triggered from one client/tab (storing one clientNonce), and verify is invoked from a different client/tab/device supplying a different (or missing) clientNonce. Also if the SPA loses the in-memory nonce between send and verify (page reload cleared state).
Common situations: User requested the magic link on desktop but clicked/entered the code on mobile. A full page reload between send and verify dropped the clientNonce. Two tabs each generated their own nonce.
Related errors
- invalid_email_token
- action_forbidden
- wrong_sign_in_credentials
- email_token_not_found
- unsupported_client_version
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/975ad2ff554ed4c1.
Report an issue: GitHub.