{"record":{"id":"46dc0982afc20960","repo":"toeverything/AFFiNE","slug":"action-forbidden-46dc09","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/core/auth/magic-link.ts","lineNumber":42,"sourceCode":"\n  constructor(\n    private readonly url: URLHelper,\n    private readonly auth: AuthService,\n    private readonly models: Models,\n    private readonly config: Config,\n    private readonly crypto: CryptoHelper\n  ) {}\n\n  async send(\n    email: string,\n    callbackUrl = '/magic-link',\n    clientNonce?: string,\n    metadata?: Pick<MailDeliveryMetadata, 'source'>\n  ) {\n    validators.assertValidEmail(email);\n\n    if (!this.url.isAllowedCallbackUrl(callbackUrl)) {\n      throw new ActionForbidden();\n    }\n\n    const callbackUrlObj = this.url.url(callbackUrl);\n    const redirectUriInCallback =\n      callbackUrlObj.searchParams.get('redirect_uri');\n    if (\n      redirectUriInCallback &&\n      !this.url.isAllowedRedirectUri(redirectUriInCallback)\n    ) {\n      throw new ActionForbidden();\n    }\n\n    const user = await this.models.user.getUserByEmail(email, {\n      withDisabled: true,\n    });\n\n    if (!user) {\n      await this.assertSignupAllowed(email);","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/magic-link.ts#L24-L60","documentation":"MagicLinkAuthService.send rejects the supplied callbackUrl because URLHelper.isAllowedCallbackUrl returns false. A callback URL is allowed only if it is a same-app relative path (starts with a single '/') or an absolute URL whose origin is in the server's configured allowedOrigins, uses an allowed protocol, and carries no userinfo. Thrown as ActionForbidden (action_forbidden category) before any user lookup or email is sent, so it is a request-shape / redirect-safety failure, not an auth failure.","triggerScenarios":"Calling the magic-link send API with a callbackUrl that is empty, uses a protocol not in ALLOWED_REDIRECT_PROTOCOLS, includes credentials (user:pass@), is a protocol-relative URL ('//evil'), or points to an origin not listed in the server's allowedOrigins / URL_SAFE_PATTERN config.","commonSituations":"Frontend points callbackUrl at a different domain than the one the server was deployed with (mismatched AFFiNE_SERVER_URL / allowed origins). A developer passed a fully external callback URL in local dev without configuring origins. A malicious or copy-pasted link contains a protocol-relative or javascript: URL.","solutions":["Use a same-origin relative callbackUrl such as '/magic-link' so the check passes unconditionally.","Add the callback origin to the server's allowed origins config (AFFiNE_SERVER_URL / url allowedOrigins) and redeploy.","Verify the URL has no embedded credentials and uses http/https.","Validate the callbackUrl client-side against the configured origin before invoking the API."],"exampleFix":"// before\nmagicLink.send(email, 'https://other-app.example/cb')\n// after — relative path, always allowed\nmagicLink.send(email, '/magic-link')","handlingStrategy":"validation","validationCode":"function buildCallbackUrl(path: string): string {\n  // only ever pass same-origin relative paths\n  if (!path.startsWith('/') || path.startsWith('//')) {\n    throw new Error('callbackUrl must be a same-origin relative path');\n  }\n  return path;\n}\n\nawait magicLink.send(email, buildCallbackUrl('/magic-link'));","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Default callbackUrl to a relative path like '/magic-link'.","Maintain the server's allowedOrigins to match all deployed frontend origins.","Reject user-supplied absolute callback URLs at the client boundary.","Never embed credentials in callback URLs."],"tags":["auth","magic-link","redirect-safety","open-redirect","config"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}