toeverything/AFFiNE · error · MissingOauthQueryParameter

missing_oauth_query_parameter

missing_oauth_query_parameter

Error message

Missing query parameter `client_nonce`.

What it means

POST /oauth/preflight validates its JSON body with OAuthPreflightBodySchema (zod, strict). When the `client_nonce` field fails validation (missing, empty string, longer than 512 chars, or not a string), the server maps it to MissingOauthQueryParameter with name 'client_nonce'. Despite the legacy 'query parameter' wording, this is a request-body field required as an anti-replay nonce for the OAuth preflight.

Source

Thrown at packages/backend/server/src/plugins/oauth/controller.ts:47

@Controller('/api/oauth')
export class OAuthController {
  constructor(
    private readonly sessionIssuer: SessionIssuer,
    private readonly oauth: OAuthService,
    private readonly providerFactory: OAuthProviderFactory,
    private readonly url: URLHelper
  ) {}

  @Public()
  @UseNamedGuard('version')
  @Post('/preflight')
  @HttpCode(HttpStatus.OK)
  async preflight(@Req() req: Request, @Body() body?: unknown) {
    const input = OAuthPreflightBodySchema.safeParse(body);
    if (!input.success) {
      const fields = new Set(input.error.issues.map(issue => issue.path[0]));
      if (fields.has('client_nonce')) {
        throw new MissingOauthQueryParameter({ name: 'client_nonce' });
      }
      if (fields.has('client')) {
        throw new ActionForbidden();
      }
      if (fields.has('provider')) {
        const provider =
          body && typeof body === 'object' && 'provider' in body
            ? String(body.provider)
            : '';
        throw new UnknownOauthProvider({ name: provider });
      }
      throw new MissingOauthQueryParameter({ name: 'provider' });
    }

    const {
      provider: unknownProviderName,
      redirect_uri: redirectUri,
      client,

View on GitHub (pinned to 591f874dad)

Solutions

  1. Include client_nonce in the preflight body: a fresh random string, 1-512 chars (crypto.randomUUID() is ideal), regenerated for every login attempt
  2. Upgrade the client/frontend to a version matching the server's OAuth preflight contract
  3. If writing a custom client, mirror the schema exactly: { provider, client, redirect_uri?, client_nonce } with no extra keys (schema is strict)

Example fix

// before
await fetch('/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'Google', client: 'web' }) });

// after
await fetch('/oauth/preflight', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ provider: 'Google', client: 'web', client_nonce: crypto.randomUUID() }),
});
Defensive patterns

Strategy: validation

Validate before calling

const PreflightBody = z.object({
  provider: z.enum(['Google', 'GitHub', 'Apple', 'OIDC']),
  redirect_uri: z.string().min(1).max(2048).nullish(),
  client: z.enum(['web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev']),
  client_nonce: z.string().min(1).max(512),
});
// reject before the request
const body = PreflightBody.parse({ provider, client, redirect_uri, client_nonce: crypto.randomUUID() });
await post('/oauth/preflight', body);

Type guard

function isPreflightBodyOk(b: unknown): b is { provider: string; client: string; client_nonce: string } {
  return (
    typeof b === 'object' && b !== null &&
    typeof (b as any).client_nonce === 'string' && (b as any).client_nonce.length >= 1 && (b as any).client_nonce.length <= 512
  );
}

Try / catch

try { await post('/oauth/preflight', body); } catch (e) {
  if ((e as any).code === 'missing_oauth_query_parameter' && (e as any).args?.name === 'client_nonce') {
    body.client_nonce = crypto.randomUUID(); // regenerate and retry once
    return post('/oauth/preflight', body);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /oauth/preflight with body lacking client_nonce, client_nonce: '', a >512-char nonce, or a non-string nonce. Custom scripts/curl calls or older client builds that predate the client_nonce requirement hit this.

Common situations: Server upgraded to a version requiring per-attempt client_nonce while an older AFFiNE client or custom integration still posts {provider, client, redirect_uri}; curl testing without the field; nonce generator returning undefined after a refactor.

Related errors


AI-assisted analysis of toeverything/AFFiNE@591f874dad (2026-08-18). Data as JSON: /api/errors/9b63a33a20e1b59f. Report an issue: GitHub.