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

Thrown by SessionExchangeService.exchange when the consumed challenge payload has no userId. Category 'bad_request', code 'invalid_auth_state'. challenges.consume returned undefined or a SessionExchangePayload without userId, meaning the one-time 'auth_session_exchange' code was missing, expired (60s TTL), already consumed, or stored without a userId.

Source

Thrown at packages/backend/server/src/core/auth/session-exchange.ts:87

  ) {}

  async createCode(req: Request, userId: string, clientVersion?: string) {
    if (!isNativeClientRequest(req)) return;
    return this.challenges.create<SessionExchangePayload>(
      'auth_session_exchange',
      { userId, clientVersion },
      60 * 1000
    );
  }

  @Transactional()
  async exchange(req: Request, code: string, metadata: AuthSessionMetadata) {
    if (!isNativeClientRequest(req)) throw new ActionForbidden();
    const payload = await this.challenges.consume<SessionExchangePayload>(
      'auth_session_exchange',
      code
    );
    if (!payload?.userId) throw new InvalidAuthState();
    const user = await this.models.user.lockForAuthIssuance(payload.userId);
    if (!user || user.disabled) throw new InvalidAuthState();
    const userSession = await this.auth.createUserSession(
      payload.userId,
      undefined,
      undefined,
      payload.clientVersion
    );

    const issued = await this.authSessions.create({
      userSessionId: userSession.id,
      ...metadata,
    });
    return this.tokenPair(
      payload.userId,
      issued.session.id,
      issued.refreshToken,
      issued.refreshExpiresAt,

View on GitHub (pinned to 26c515e050)

Solutions

  1. Re-initiate the session exchange flow to obtain a fresh one-time code and exchange it promptly (within 60s).
  2. Verify the code was actually created by createCode (which requires a native-client request) before calling exchange.
  3. Ensure the challenge store backend (cache/redis) is reachable and not evicting keys prematurely.

Example fix

// before: reuse a stale code
const { accessToken } = await exchange(req, oldCode, metadata);

// after: mint a fresh code and exchange immediately
const { code } = await createCode(req, userId, clientVersion);
if (!code) throw new Error('Native client required to create a code');
const { accessToken } = await exchange(req, code, metadata);
Defensive patterns

Strategy: retry

Type guard

function hasUserId(p: unknown): p is SessionExchangePayload {
  return !!p && typeof (p as SessionExchangePayload).userId === 'string';
}

Try / catch

try {
  await exchange(req, code, metadata);
} catch (e) {
  if (e.code === 'invalid_auth_state') {
    const fresh = await createCode(req, userId, clientVersion);
    if (fresh) await exchange(req, fresh, metadata); // one retry with a fresh code
  } else throw e;
}

Prevention

When it happens

Trigger: Calling exchange (session-exchange.ts:83-87) with a code that the challenge store cannot resolve to a valid SessionExchangePayload, or whose payload.userId is absent. The code is created by createCode with a 60_000ms TTL.

Common situations: The user started auth on device A (which created the code) but the exchange is attempted after the 60s window elapsed; the code was already exchanged once (single-use); a code was crafted/tampered with so it lacks userId; createCode never ran because createCode itself is a no-op for non-native requests.

Related errors


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