{"record":{"id":"9b63a33a20e1b59f","repo":"toeverything/AFFiNE","slug":"missing-oauth-query-parameter-9b63a3","errorCode":"missing_oauth_query_parameter","errorMessage":"Missing query parameter `client_nonce`.","messagePattern":"Missing query parameter `client_nonce`\\.","errorType":"exception","errorClass":"MissingOauthQueryParameter","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/controller.ts","lineNumber":47,"sourceCode":"@Controller('/api/oauth')\nexport class OAuthController {\n  constructor(\n    private readonly sessionIssuer: SessionIssuer,\n    private readonly oauth: OAuthService,\n    private readonly providerFactory: OAuthProviderFactory,\n    private readonly url: URLHelper\n  ) {}\n\n  @Public()\n  @UseNamedGuard('version')\n  @Post('/preflight')\n  @HttpCode(HttpStatus.OK)\n  async preflight(@Req() req: Request, @Body() body?: unknown) {\n    const input = OAuthPreflightBodySchema.safeParse(body);\n    if (!input.success) {\n      const fields = new Set(input.error.issues.map(issue => issue.path[0]));\n      if (fields.has('client_nonce')) {\n        throw new MissingOauthQueryParameter({ name: 'client_nonce' });\n      }\n      if (fields.has('client')) {\n        throw new ActionForbidden();\n      }\n      if (fields.has('provider')) {\n        const provider =\n          body && typeof body === 'object' && 'provider' in body\n            ? String(body.provider)\n            : '';\n        throw new UnknownOauthProvider({ name: provider });\n      }\n      throw new MissingOauthQueryParameter({ name: 'provider' });\n    }\n\n    const {\n      provider: unknownProviderName,\n      redirect_uri: redirectUri,\n      client,","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/591f874dad30887a80143a061a44bd3ca7ee3299/packages/backend/server/src/plugins/oauth/controller.ts#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Include client_nonce in the preflight body: a fresh random string, 1-512 chars (crypto.randomUUID() is ideal), regenerated for every login attempt","Upgrade the client/frontend to a version matching the server's OAuth preflight contract","If writing a custom client, mirror the schema exactly: { provider, client, redirect_uri?, client_nonce } with no extra keys (schema is strict)"],"exampleFix":"// before\nawait fetch('/oauth/preflight', { method: 'POST', body: JSON.stringify({ provider: 'Google', client: 'web' }) });\n\n// after\nawait fetch('/oauth/preflight', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ provider: 'Google', client: 'web', client_nonce: crypto.randomUUID() }),\n});","handlingStrategy":"validation","validationCode":"const PreflightBody = z.object({\n  provider: z.enum(['Google', 'GitHub', 'Apple', 'OIDC']),\n  redirect_uri: z.string().min(1).max(2048).nullish(),\n  client: z.enum(['web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev']),\n  client_nonce: z.string().min(1).max(512),\n});\n// reject before the request\nconst body = PreflightBody.parse({ provider, client, redirect_uri, client_nonce: crypto.randomUUID() });\nawait post('/oauth/preflight', body);","typeGuard":"function isPreflightBodyOk(b: unknown): b is { provider: string; client: string; client_nonce: string } {\n  return (\n    typeof b === 'object' && b !== null &&\n    typeof (b as any).client_nonce === 'string' && (b as any).client_nonce.length >= 1 && (b as any).client_nonce.length <= 512\n  );\n}","tryCatchPattern":"try { await post('/oauth/preflight', body); } catch (e) {\n  if ((e as any).code === 'missing_oauth_query_parameter' && (e as any).args?.name === 'client_nonce') {\n    body.client_nonce = crypto.randomUUID(); // regenerate and retry once\n    return post('/oauth/preflight', body);\n  }\n  throw e;\n}","preventionTips":["Generate client_nonce fresh (crypto.randomUUID) for every preflight call — never cache or reuse it","Keep a shared zod schema for the preflight body in the client and server codebases so contracts can't drift","Send Content-Type: application/json; the schema expects a JSON body, not query strings"],"tags":["oauth","preflight","client-nonce","validation","request-body"],"backgroundTag":"missing-request-parameter","analyzedSha":"591f874dad30887a80143a061a44bd3ca7ee3299","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}