|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +'@objectstack/plugin-auth': minor |
| 4 | +'@objectstack/runtime': minor |
| 5 | +'@objectstack/mcp': minor |
| 6 | +--- |
| 7 | + |
| 8 | +feat(mcp): spec-compliant OAuth 2.1 authorization for `/api/v1/mcp` (#2698) |
| 9 | + |
| 10 | +Any OAuth-capable MCP client (claude.ai custom connectors, Claude Desktop, |
| 11 | +Claude Code) can now connect to a deployment **self-serve**: no admin-minted |
| 12 | +API key, no central registry — you sign in through the browser as yourself and |
| 13 | +every tool call runs under your own permissions and row-level security. |
| 14 | + |
| 15 | +**Each deployment is its own authorization server**, backed by the embedded |
| 16 | +better-auth instance (`@better-auth/oauth-provider`). Rationale for the design |
| 17 | +decisions lives in #2698; the moving parts: |
| 18 | + |
| 19 | +- **Discovery**: `/.well-known/oauth-protected-resource` (RFC 9728, incl. the |
| 20 | + path-inserted variant for `/api/v1/mcp`) and |
| 21 | + `/.well-known/oauth-authorization-server` (RFC 8414, incl. the path-inserted |
| 22 | + variant for the `/api/v1/auth` issuer) are served from the deployment origin. |
| 23 | + 401s from `/api/v1/mcp` advertise the resource metadata via |
| 24 | + `WWW-Authenticate`, so clients bootstrap the flow automatically. |
| 25 | +- **Dynamic Client Registration (RFC 7591)** is enabled (unauthenticated, as |
| 26 | + the MCP spec requires) whenever the MCP surface is on — every deployment is a |
| 27 | + distinct AS, so clients cannot ship pre-registered IDs. Force it either way |
| 28 | + with `OS_OIDC_DCR_ENABLED` or the new `plugins.dynamicClientRegistration` |
| 29 | + auth-config field. The embedded AS itself now auto-enables when |
| 30 | + `OS_MCP_SERVER_ENABLED=true` (explicit `OS_OIDC_PROVIDER_ENABLED=false` still |
| 31 | + wins). |
| 32 | +- **Authorization-code + PKCE** flow with RFC 8707 resource binding: access |
| 33 | + tokens are minted with `aud=<origin>/api/v1/mcp` and verified locally |
| 34 | + (signature/issuer/audience/expiry) against the deployment's own JWKS — |
| 35 | + fail-closed parity with API keys: unknown/expired/wrong-audience tokens, |
| 36 | + sub-less M2M tokens, or a presented-but-invalid bearer never fall back to an |
| 37 | + ambient session, they 401. |
| 38 | +- **Token → ExecutionContext**: a valid access token resolves to the same |
| 39 | + principal-bound `ExecutionContext` as every other credential, single-sourced |
| 40 | + through `resolveAuthzContext` — OAuth adds a second *provenance* for the |
| 41 | + principal, not a second authz model. `ExecutionContext` gains an optional |
| 42 | + `oauthScopes` field carrying the token's granted scopes. |
| 43 | +- **Coarse scopes → tool families**, enforced at tool dispatch: `data:read` |
| 44 | + (list/describe/query/get), `data:write` (create/update/delete), |
| 45 | + `actions:execute` (list_actions/run_action). Constants live in |
| 46 | + `@objectstack/spec/ai` (`MCP_OAUTH_SCOPES`). Tools outside the grant are not |
| 47 | + registered — and therefore rejected — for that request. API-key and session |
| 48 | + principals are unaffected (not scope-limited). |
| 49 | +- **TLS required, localhost exempt** (OAuth 2.1): on a plain-HTTP non-loopback |
| 50 | + origin the OAuth track stays dark (no metadata, no bearer acceptance) and the |
| 51 | + endpoint remains API-key-only. Local clients reach intranet deployments; |
| 52 | + claude.ai web connectors additionally need public HTTPS reachability. |
| 53 | + |
| 54 | +**API keys are unchanged** (dual-track): `x-api-key` / `Authorization: ApiKey` / |
| 55 | +`Authorization: Bearer osk_…` keep working exactly as before for CI and |
| 56 | +headless agents — covered by new regression tests. |
0 commit comments