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 `AuthChallengeStore.create` when `isValidCacheTtl(ttlMs)` is false — i.e. `ttlMs` is not a positive safe integer. The challenge (oauth state, open-app sign-in code, captcha, passkey) cannot be stored with an invalid expiry, so creation aborts. HTTP 400.

Source

Thrown at packages/backend/server/src/core/auth/challenge-store.ts:26

export type AuthChallengePurpose =
  | 'oauth_state'
  | 'open_app_sign_in'
  | 'auth_session_exchange'
  | 'captcha'
  | 'passkey_registration'
  | 'passkey_authentication';

@Injectable()
export class AuthChallengeStore {
  constructor(private readonly cache: SessionCache) {}

  async create<T>(
    purpose: AuthChallengePurpose,
    payload: T | ((token: string) => T),
    ttlMs: number
  ): Promise<string> {
    if (!isValidCacheTtl(ttlMs)) {
      throw new InvalidAuthState();
    }

    const token = randomUUID();
    const value =
      typeof payload === 'function'
        ? (payload as (token: string) => T)(token)
        : payload;
    const stored = await this.cache.set(this.key(purpose, token), value, {
      ttl: ttlMs,
    });
    if (!stored) {
      throw new InvalidAuthState();
    }
    return token;
  }

  async get<T>(purpose: AuthChallengePurpose, token: string) {
    return (await this.cache.get<T>(this.key(purpose, token))) ?? null;

View on GitHub (pinned to 26c515e050)

Solutions

  1. Pass a positive integer milliseconds value, e.g. `60 * 1000`.
  2. Validate the TTL at the call site before calling `create`: `Number.isSafeInteger(ttl) && ttl > 0`.
  3. Audit the config source feeding the TTL (env var parsing) to ensure it produces a number.

Example fix

// before
await challenges.create('captcha', payload, cfg.captchaTtl /* string '120000' */);

// after
const ttl = Number(cfg.captchaTtl);
if (!Number.isSafeInteger(ttl) || ttl <= 0) throw new Error('bad ttl');
await challenges.create('captcha', payload, ttl);
Defensive patterns

Strategy: validation

Validate before calling

function validTtl(ttl: unknown): ttl is number {
  return Number.isSafeInteger(ttl) && (ttl as number) > 0;
}
if (!validTtl(ttlMs)) throw new Error(`ttl must be a positive integer, got ${ttlMs}`);
await challenges.create('captcha', payload, ttlMs);

Type guard

function isValidCacheTtl(ttl: unknown): ttl is number {
  return typeof ttl === 'number' && Number.isSafeInteger(ttl) && ttl > 0;
}

Prevention

When it happens

Trigger: A caller passes `ttlMs` as 0, a negative number, a fractional value, `undefined`, `Infinity`, or a non-number (e.g. a string TTL read from config). Any new code path that constructs a challenge without validating the TTL will hit this.

Common situations: Config typo where a TTL is expressed as a string (`'60000'`) instead of a number, a refactor that drops the TTL argument, or a feature flag that yields `NaN`.

Related errors


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