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

  1. Mint a fresh internal access token per request via `CryptoHelper.parseInternalAccessToken`'s counterpart, including current timestamp, method, path, and a unique nonce.
  2. Sync clocks (NTP) on all services to stay within the 30s skew window.
  3. Do not retry a request with the same token/nonce — generate a new one each time.
  4. 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

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.