Auth

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 #

  1. The client fetches /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server (src/routes/oauth-discovery.ts).
  2. It registers (POST /register, a stub that returns the one pre-provisioned Clerk OAuth app; in dev_local it reports client_secret_post because the app is confidential, deployed environments still report none), sends the user to GET /authorize (proxied to Clerk), and exchanges the code at POST /token (proxied to Clerk, src/routes/oauth-proxy.ts).
  3. On every tool call the client sends Authorization: Bearer <Clerk OAuth token>. src/middleware/auth.ts verifies it (JWKS, issuer, client_id), then forwards it unchanged to bonsapi.
  4. bonsapi verifies the token again with its own verifier and enforces the user’s real org and entity permissions. In dev_local it needs CLERK_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.