toeverything/AFFiNE · error · ActionForbidden
action_forbidden
action_forbidden
Error message
You are not allowed to perform this action.
What it means
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.
Source
Thrown at packages/backend/server/src/core/auth/magic-link.ts:42
constructor(
private readonly url: URLHelper,
private readonly auth: AuthService,
private readonly models: Models,
private readonly config: Config,
private readonly crypto: CryptoHelper
) {}
async send(
email: string,
callbackUrl = '/magic-link',
clientNonce?: string,
metadata?: Pick<MailDeliveryMetadata, 'source'>
) {
validators.assertValidEmail(email);
if (!this.url.isAllowedCallbackUrl(callbackUrl)) {
throw new ActionForbidden();
}
const callbackUrlObj = this.url.url(callbackUrl);
const redirectUriInCallback =
callbackUrlObj.searchParams.get('redirect_uri');
if (
redirectUriInCallback &&
!this.url.isAllowedRedirectUri(redirectUriInCallback)
) {
throw new ActionForbidden();
}
const user = await this.models.user.getUserByEmail(email, {
withDisabled: true,
});
if (!user) {
await this.assertSignupAllowed(email);View on GitHub (pinned to 26c515e050)
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.
Example fix
// before magicLink.send(email, 'https://other-app.example/cb') // after — relative path, always allowed magicLink.send(email, '/magic-link')
Defensive patterns
Strategy: validation
Validate before calling
function buildCallbackUrl(path: string): string {
// only ever pass same-origin relative paths
if (!path.startsWith('/') || path.startsWith('//')) {
throw new Error('callbackUrl must be a same-origin relative path');
}
return path;
}
await magicLink.send(email, buildCallbackUrl('/magic-link')); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unsupported_client_version
- wrong_sign_in_credentials
- invalid_auth_state
- invalid_email_token
- sign_up_forbidden
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/46dc0982afc20960.
Report an issue: GitHub.