{"record":{"id":"9adbef7f73af2c17","repo":"toeverything/AFFiNE","slug":"action-forbidden-9adbef","errorCode":"action_forbidden","errorMessage":"You are not allowed to perform this action.","messagePattern":"You are not allowed to perform this action\\.","errorType":"exception","errorClass":"ActionForbidden","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/controller.ts","lineNumber":50,"sourceCode":"    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,\n      client_nonce: clientNonce,\n    } = input.data;\n","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/591f874dad30887a80143a061a44bd3ca7ee3299/packages/backend/server/src/plugins/oauth/controller.ts#L32-L68","documentation":"During POST /oauth/preflight, if the `client` field fails the OAuthClientSchema enum ('web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev'), the server deliberately throws a generic ActionForbidden instead of revealing the valid client list. So 'not allowed to perform this action' here almost always means 'unrecognized OAuth client identifier'.","triggerScenarios":"Preflight body with client missing or set to anything outside the enum — e.g. client: 'mobile', 'my-app', 'ios', or a typo like 'Affine'. A missing client also routes here because the zod issue path is 'client'.","commonSituations":"Building a third-party client against the AFFiNE OAuth endpoints with a custom client name; older desktop build whose client identifier was removed from the enum; frontend refactor passing the wrong variable.","solutions":["Set client to one of the allowed values: 'web' (browser), 'affine' (stable desktop), 'affine-canary', 'affine-beta', or 'affine-dev'","If you are building a custom integration, use 'web' and handle the flow in a browser context","Upgrade the desktop/electron client to a version whose identifier the server still accepts"],"exampleFix":"// before\n{ provider: 'Google', client: 'desktop', client_nonce: nonce }\n\n// after\n{ provider: 'Google', client: 'affine', client_nonce: nonce }","handlingStrategy":"validation","validationCode":"const ALLOWED_CLIENTS = ['web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev'] as const;\nif (!ALLOWED_CLIENTS.includes(client)) {\n  throw new Error(`client must be one of ${ALLOWED_CLIENTS.join(', ')} — got '${client}'`);\n}","typeGuard":"type AllowedClient = 'web' | 'affine' | 'affine-canary' | 'affine-beta' | 'affine-dev';\nfunction isAllowedClient(c: unknown): c is AllowedClient {\n  return typeof c === 'string' && ['web', 'affine', 'affine-canary', 'affine-beta', 'affine-dev'].includes(c);\n}","tryCatchPattern":"try { await post('/oauth/preflight', body); } catch (e) {\n  if ((e as any).code === 'action_forbidden' && !isAllowedClient(body.client)) {\n    throw new Error(`Server rejected client '${body.client}' — use an allowed client identifier`);\n  }\n  throw e;\n}","preventionTips":["Hard-code the allowed client enum in your integration and type it, so invalid values can't be sent","Remember ActionForbidden on preflight deliberately hides the reason — first suspect is always the client field","Track server release notes: client identifiers have been added/removed between versions"],"tags":["oauth","preflight","client-identifier","forbidden"],"backgroundTag":"unauthorized-client","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"}