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 `POST /api/auth/open-app/sign-in` when the body fails `OpenAppSignInBodySchema.safeParse`. The schema is `{ code: z.string().min(1).max(512) }.strict()`, so a missing/empty/too-long `code`, an extra field, or a non-object body all fail. HTTP 400 (presented as invalid_auth_state because the one-time code is the auth state).

Source

Thrown at packages/backend/server/src/core/auth/controller.ts:232

  @Public()
  @UseNamedGuard('version')
  @Post('/open-app/sign-in-code')
  async openAppSignInCode(@CurrentUser() user?: CurrentUser) {
    if (!user) throw new ActionForbidden();
    const code = await this.openApp.createSignInCode(user);
    return { code };
  }

  @Public()
  @UseNamedGuard('version')
  @Post('/open-app/sign-in')
  async openAppSignIn(
    @Req() req: Request,
    @Res() res: Response,
    @Body() body?: unknown
  ) {
    const credential = OpenAppSignInBodySchema.safeParse(body);
    if (!credential.success) throw new InvalidAuthState();
    const identity = await this.openApp.verifySignInCode(credential.data.code);
    const { exchangeCode } = await this.sessionIssuer.issue(req, res, identity);
    res.send({ id: identity.userId, exchangeCode });
  }

  @Public()
  @UseNamedGuard('version')
  @Post('/session/exchange')
  @Header('Cache-Control', 'no-store')
  @Header('Pragma', 'no-cache')
  async exchangeSession(@Req() req: Request, @Body() body?: unknown) {
    const input = AuthSessionExchangeBodySchema.parse(body);
    return await this.sessionExchange.exchange(req, input.code, {
      installationId: input.installationId,
      platform: input.platform,
      deviceName: input.deviceName,
      appVersion: getClientVersionFromRequest(req) ?? undefined,
    });

View on GitHub (pinned to 26c515e050)

Solutions

  1. Send exactly `{ code: string }` with a 1–512 char code obtained from `/open-app/sign-in-code`.
  2. Validate the code is non-empty on the client before posting.
  3. Regenerate the code if it was already consumed or expired (codes live 60s).

Example fix

// before
fetch('/api/auth/open-app/sign-in', { method: 'POST', body: '{}' });

// after
fetch('/api/auth/open-app/sign-in', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ code }),
});
Defensive patterns

Strategy: validation

Validate before calling

import { z } from 'zod';
const Body = z.object({ code: z.string().min(1).max(512) }).strict();
const parsed = Body.safeParse(payload);
if (!parsed.success) throw new Error('code required (1-512 chars)');
await fetch('/api/auth/open-app/sign-in', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(parsed.data),
});

Type guard

function isOpenAppSignInBody(v: unknown): v is { code: string } {
  return typeof v === 'object' && v !== null &&
    typeof (v as any).code === 'string' &&
    (v as any).code.length >= 1 && (v as any).code.length <= 512;
}

Prevention

When it happens

Trigger: Posting `{}` or `{ code: '' }`, omitting the code from the desktop handoff, replaying a code format the strict schema rejects, or attaching extra fields.

Common situations: Desktop client sends an empty code before the browser generated one, the code query param was lost in the deep-link handoff, or a client version sends a different payload shape.

Related errors


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