toeverything/AFFiNE · error · ActionForbidden
action_forbidden
action_forbidden
Error message
You are not allowed to perform this action.
What it means
MagicLinkService.send validates the callbackUrl argument against UrlService.isAllowedCallbackUrl before sending a sign-in email; URLs not on the server-configured allowlist throw ActionForbidden (action_forbidden). The callback URL is where the user lands after clicking the magic link, and the allowlist exists to stop open-redirect / phishing abuse of the mail flow.
Solutions
- Use the default relative callback '/magic-link' unless you specifically configured another
- Add your exact frontend origin/path to the server's allowed callback URL configuration and restart
- Verify the value you send matches the configured entry character-for-character (scheme, host, no trailing slash)
Example fix
// before
await post('/auth/magic-link/send', { email, callbackUrl: 'https://myapp.example/cb' });
// after
await post('/auth/magic-link/send', { email, callbackUrl: '/magic-link' }); // or an origin present in the server allowlist Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED_CALLBACK_URLS = ['/magic-link']; // mirror the server allowlist
function isAllowedCallbackUrl(url: string): boolean {
return ALLOWED_CALLBACK_URLS.includes(url);
} Prevention
- Default to '/magic-link' and only deviate after adding the target to server config
- Keep an env-driven list of allowed callbacks shared by web and server config
When it happens
Trigger: POST to the magic-link send endpoint with callbackUrl pointing at a foreign origin (https://evil.example); a self-hosted frontend origin that was never added to the server's allowed callback list; passing an absolute URL where only specific relative paths are allowed; trailing-slash or case differences that miss the allowlist match.
Common situations: Self-hosting on a new domain without updating allowed callback URL config; renaming/relocating the web app; staging environment pointing at production API; third-party portals trying to relay AFFiNE sign-in.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/46dc0982afc20960.
Report an issue: GitHub.
Appendix: 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 b4c8548c09)