toeverything/AFFiNE · error · AccessDenied
access_denied
access_denied
Error message
Invalid internal request
What it means
Thrown by the `AuthGuard` for an `@Internal()` (RPC) endpoint when the `x-access-token` is absent, fails to parse, is expired/out of the 5-minute + 30s skew window, the method/path don't match the token payload, or the nonce was already used (replay). HTTP 403 (no_permission).
Source
Thrown at packages/backend/server/src/core/auth/guard.ts:99
const method = req.method.toUpperCase();
const path = req.path;
const timestampInRange =
payload.ts <= now + INTERNAL_ACCESS_TOKEN_CLOCK_SKEW_MS &&
now - payload.ts <= INTERNAL_ACCESS_TOKEN_TTL_MS;
if (timestampInRange && payload.m === method && payload.p === path) {
const nonceKey = `rpc:nonce:${payload.nonce}`;
const ok = await this.cache.setnx(nonceKey, 1, {
ttl: INTERNAL_ACCESS_TOKEN_TTL_MS,
});
if (ok) {
return true;
}
}
}
}
throw new AccessDenied('Invalid internal request');
}
// api is public
const isPublic = this.reflector.getAllAndOverride<boolean>(
PUBLIC_ENTRYPOINT_SYMBOL,
[clazz, handler]
);
const authedUser = await this.signIn(req, res, isPublic);
if (isPublic) {
return true;
}
if (!authedUser) {
throw new AuthenticationRequired();
}
View on GitHub (pinned to 26c515e050)
Solutions
- Mint a fresh internal access token per request via `CryptoHelper.parseInternalAccessToken`'s counterpart, including current timestamp, method, path, and a unique nonce.
- Sync clocks (NTP) on all services to stay within the 30s skew window.
- Do not retry a request with the same token/nonce — generate a new one each time.
- Confirm the internal signing secret is consistent across services.
Defensive patterns
Strategy: try-catch
Try / catch
try {
await rpcCall();
} catch (e) {
if (e.code === 'access_denied') {
// regenerate token with fresh nonce + timestamp and retry once
const token = mintInternalAccessToken({ method, path, nonce: randomUUID() });
await rpcCall(token);
} else throw e;
} Prevention
- Mint a fresh internal access token per request with current timestamp, exact method/path, and a unique nonce.
- Keep service clocks NTP-synced to stay within the 30s skew window.
- Never retry with the same token/nonce — generate a new one each call.
- Keep the internal signing secret consistent across services.
When it happens
Trigger: An inter-service RPC call missing the internal access token, a token whose timestamp drifted beyond the skew/TTL, a replayed token (nonce already seen), or a token minted for a different method/path than the current request.
Common situations: Service-to-service caller forgot to attach `x-access-token`, clock skew between signer and verifier >30s, the internal-token signing secret rotated and the caller still uses the old one, or a load balancer retries an idempotent-but-nonce-guarded request.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/4b52352bef1134cf.
Report an issue: GitHub.