toeverything/AFFiNE · error · ActionForbidden
action_forbidden
action_forbidden
Error message
You are not allowed to perform this action.
What it means
Thrown by SessionExchangeService.exchange when isNativeClientRequest(req) is false. Category 'action_forbidden', code 'action_forbidden'. The exchange endpoint (which turns a one-time code into access/refresh tokens) is reserved for native clients (iOS/Android/Electron) identified by their specific client-kind header + origin; a web/browser request is rejected before any work begins.
Source
Thrown at packages/backend/server/src/core/auth/session-exchange.ts:82
private readonly challenges: AuthChallengeStore,
private readonly cache: Cache,
private readonly models: Models,
private readonly accessTokens: AccessTokenService,
private readonly authSessions: AuthSessionService
) {}
async createCode(req: Request, userId: string, clientVersion?: string) {
if (!isNativeClientRequest(req)) return;
return this.challenges.create<SessionExchangePayload>(
'auth_session_exchange',
{ userId, clientVersion },
60 * 1000
);
}
@Transactional()
async exchange(req: Request, code: string, metadata: AuthSessionMetadata) {
if (!isNativeClientRequest(req)) throw new ActionForbidden();
const payload = await this.challenges.consume<SessionExchangePayload>(
'auth_session_exchange',
code
);
if (!payload?.userId) throw new InvalidAuthState();
const user = await this.models.user.lockForAuthIssuance(payload.userId);
if (!user || user.disabled) throw new InvalidAuthState();
const userSession = await this.auth.createUserSession(
payload.userId,
undefined,
undefined,
payload.clientVersion
);
const issued = await this.authSessions.create({
userSessionId: userSession.id,
...metadata,
});View on GitHub (pinned to 26c515e050)
Solutions
- If the caller is a web app, use the web (cookie/session) auth flow instead of the native exchange endpoint.
- If the caller is a native SDK, ensure it sends the required CLIENT_KIND_HEADER and a non-browser Origin on every request.
- In tests, set the headers via the NativeClientHeadersSchema shape or use the provided test request builder.
Example fix
// before: calling exchange from a browser fetch
fetch('/auth/session/exchange', { method: 'POST', body: JSON.stringify({ code }) });
// after: native client sets the required headers
fetch('/auth/session/exchange', {
method: 'POST',
headers: { [CLIENT_KIND_HEADER]: 'ios', 'Content-Type': 'application/json' },
body: JSON.stringify({ code, metadata }),
}); Defensive patterns
Strategy: validation
Validate before calling
import { NativeClientHeadersSchema, CLIENT_KIND_HEADER } from '../../core/auth/input';
function buildNativeHeaders(clientKind: string): Record<string, string> {
const ok = NativeClientHeadersSchema.safeParse({ clientKind, origin: 'native' }).success;
if (!ok) throw new Error('Not a native client request');
return { [CLIENT_KIND_HEADER]: clientKind };
} Type guard
function isNativeClientHeaders(input: unknown): boolean {
return NativeClientHeadersSchema.safeParse(input).success;
} Try / catch
try {
await exchange(req, code, metadata);
} catch (e) {
if (e.code === 'action_forbidden' && !isNativeClientRequest(req)) {
throw new Error('Use the web session flow from browsers; exchange is native-only');
}
throw e;
} Prevention
- Keep native and web auth flows strictly separated at the client.
- Ensure the native SDK always sends CLIENT_KIND_HEADER and Origin.
- Configure ingress to preserve both headers without rewriting.
When it happens
Trigger: Calling the session-exchange `exchange` operation from a browser, curl, or any HTTP client whose request headers do not satisfy NativeClientHeadersSchema ({ clientKind, origin }). The guard at session-exchange.ts:82 fires before the challenge code is consumed.
Common situations: A web frontend mistakenly calling the native token-exchange endpoint instead of the web session flow; integration tests that omit the native client headers; a reverse proxy stripping the CLIENT_KIND_HEADER or Origin header.
Related errors
- invalid_auth_state
- action_forbidden
- too_many_request
- The refresh token is invalid. | The auth session has expired
- auth_session_revoked
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/88a3973719027b85.
Report an issue: GitHub.