Auth #
Classification: Restricted (describes source code, infrastructure, and access control).
Single path today: Clerk OAuth token pass-through. src/middleware/auth.ts verifies the token (JWKS, issuer, client_id pinned to this server’s dedicated Clerk OAuth app — Clerk puts no aud on OAuth tokens) then, unlike the internal server, keeps it and forwards it to BonsAPI on every downstream call (src/bonsapi/custom-fetch.ts).
Known gap: BonsAPI’s verifier (libs/rust/bonsai-clerk/src/model/jwt.rs) rejects any OAuth token with no org_id claim — Clerk only stamps org_id when the signed-in user has an active organization. A caller with no active org gets a clean 401 (that’s the spec’d behavior, not a bug — see ENG-6794), but it means a real document/permission fetch needs a Clerk session with an active org, not just any valid login.
Service-key path: removed (ENG-7010). The tofu-api-key service-key credential path designed in ENG-6796 was retired before any key was ever issued (zero live traffic) — this server is JWT-only (Clerk OAuth) end to end now. src/service-key-policy.ts, the middleware’s API-key branch, and the per-tool bonsapiOperationId opt-in have all been deleted. See Service-key policy (retired) for the retired design.
Org gating: removed (ENG-8450). The per-org is_external_mcp_enabled feature flag (ENG-7562, itself a replacement for the older TOFU_EXTERNAL_MCP_ALLOWED_ORG_IDS env-var allowlist) has been deleted outright rather than left at its ready-tier self-serve default — every org that clears the identity checks above reaches tool dispatch, with no per-org toggle, Settings UI entry, or Clerk-metadata lookup left in the path. src/middleware/org-mcp-access.ts is gone; src/middleware/auth.ts no longer makes any bonsapi call to resolve access before next().
The OAuth flow #
- The client fetches
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server(src/routes/oauth-discovery.ts). - It registers (
POST /register, a stub that returns the one pre-provisioned Clerk OAuth app; indev_localit reportsclient_secret_postbecause the app is confidential, deployed environments still reportnone), sends the user toGET /authorize(proxied to Clerk), and exchanges the code atPOST /token(proxied to Clerk,src/routes/oauth-proxy.ts). - On every tool call the client sends
Authorization: Bearer <Clerk OAuth token>.src/middleware/auth.tsverifies it (JWKS, issuer,client_id), then forwards it unchanged to bonsapi. - bonsapi verifies the token again with its own verifier and enforces the user’s real org and entity permissions. In
dev_localit needsCLERK_OAUTH_CLIENT_ID, otherwise it rejects every OAuth token.
For a user who is already signed in, Clerk sends the browser to the webapp’s /sign-in?redirect_url=… and the webapp must pass that URL through to reach the consent screen. apps/webapp/src/shared/lib/auth/trusted-redirect.ts trusts the Clerk Frontend API host and the Account Portal host (derived from the publishable key; prod clerk.<domain> → accounts.<domain>, limited to /oauth/* and /oauth-consent). A rejected redirect_url logs [auth] untrusted redirect_url dropped with its host.
In dev_local the Clerk OAuth app is confidential, so the client must send client_secret (client_secret_post) at /token; deployed environments still register a public client. The proxy forwards the request body unchanged and never injects a secret. /register cannot hand one out, so a client that only registers dynamically (for example MCP Inspector) must be given the secret out of band, as the Claude Code connect task does. A secret sent in an Authorization header (client_secret_basic) is not forwarded and not advertised. The proxy keeps no state.
What is advertised differs in dev_local only (issuer, user:org:read, loopback redirects); see Environments.