From 989e59233c69fc780b2fad0e07b57a5dfee6ef8a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 15 Aug 2026 14:01:54 +0200 Subject: [PATCH] Document MCP authorization design --- CHANGELOG.md | 5 +++++ README.md | 12 ++++++++---- docs/architecture.md | 14 ++++++++++---- docs/errors.md | 16 +++++++++++++++ docs/mcp-conformance.md | 11 ++++++++++- docs/security.md | 43 +++++++++++++++++++++++++++++++++++++++++ src/mcp.zig | 22 +++++++++++++++++++++ 7 files changed, 114 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db7d673..26b9f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add MCP `2026-07-28` HTTP authorization and deployment security with bounded + RFC 9728/RFC 8414/OIDC discovery, exact issuer and resource binding, PKCE + checks, owned token callbacks, bounded refresh and scope step-up, pre-dispatch + TLS/Origin/Host enforcement, owned challenges, redirect isolation, redaction, + allocation-failure tests, and fuzzing. - Complete the MCP `2026-07-28` conformance matrix with strict nested message and metadata validation, stateless capability guards, MRTR validation, filtered acknowledgement-first subscriptions, bounded cycle-safe tool diff --git a/README.md b/README.md index c9acf2e..c395b40 100644 --- a/README.md +++ b/README.md @@ -541,12 +541,16 @@ defer if (settings) |json| allocator.free(json); `zigai.mcp.Server` provides the matching transport-neutral server dispatcher, automatic `server/discover`, protocol and HTTP header validation, extension dispatch, result metadata, and a stdio serving loop. HTTP hosts pass their -request headers to `Server.handle`; authorization remains an HTTP concern and -can be configured with `mcp_http.headers`. Core handlers run only when the -matching capability appears in the server's `capabilities_json`. +request headers and TLS state to `Server.handle`. Protected endpoints use +`mcp.auth.ClientPolicy` and `mcp.auth.ServerPolicy`; browser-facing hosts add a +separate `mcp.auth.DeploymentPolicy` for exact Origin and Host checks. Bearer +tokens never enter MCP JSON or handler parameters, and refresh/step-up retries +are bounded. Core handlers run only when the matching capability appears in +the server's `capabilities_json`. See the [MCP conformance matrix](docs/mcp-conformance.md) for message coverage, -compatibility boundaries, validation, and ownership. +compatibility boundaries, validation, and ownership. The [security guide](docs/security.md#mcp-http-authorization) +covers OAuth discovery, token callbacks, response headers, and deployment. ### Approval and deferred tools diff --git a/docs/architecture.md b/docs/architecture.md index 4a25346..4acafb6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -351,14 +351,20 @@ notifications to an `EventSink` until the final response closes the stream. `StreamableHttpTransport` emits the required protocol, method, name, and tool parameter headers. It accepts direct JSON and request-scoped SSE responses. `StdioTransport` frames one JSON-RPC message per line and correlates response -IDs. Authentication is supplied as custom HTTP headers and is outside the MCP -message layer. +IDs. The separate `mcp.auth` module owns transport-level authorization +contracts: bounded protected-resource and authorization-server discovery, +issuer-bound token acquisition, RFC 9207 response checks, and scope step-up. +The HTTP transport adds one sensitive Bearer header per attempt; credentials +never enter the JSON-RPC envelope. `mcp.Server` is a transport-neutral dispatcher. It provides discovery, per-request version checks, standard and tool-parameter header validation, server identity metadata, core or extension method dispatch, JSON-RPC errors, -and a stdio serving loop. An HTTP application passes its request headers to -the same dispatcher. +and a stdio serving loop. An HTTP application passes request headers and TLS +state to the same dispatcher. Deployment policy rejects invalid browser +Origins, Host mismatches, and unexpected cleartext before JSON parsing; +authorization policy validates the Bearer token for the canonical resource +before application dispatch and returns owned `WWW-Authenticate` headers. ## Deferred tool execution diff --git a/docs/errors.md b/docs/errors.md index 85c3993..0e86437 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -128,6 +128,22 @@ or partially activate a bundle. | `LocalNetworkUrlForbidden` | The URL uses a local name or non-public literal IP. | | `UrlHostNotAllowed` | The URL host is absent from an explicit allowlist. | +### MCP authorization + +| Error | Meaning | +| --- | --- | +| `InvalidAuthorizationIssuer` | A token, response, or metadata document is bound to a different authorization-server issuer. | +| `MissingAuthorizationIssuer` | RFC 9207 support was advertised but the authorization response omitted `iss`. | +| `InvalidAuthorizationServerMetadata` | Discovered OAuth/OIDC metadata is missing required fields or contains an ineligible endpoint. | +| `InvalidProtectedResourceMetadata` | RFC 9728 metadata is malformed, mismatched, duplicated, or lacks Bearer-header support. | +| `InvalidResourceUri` | The configured RFC 8707 resource is not an eligible canonical endpoint URI. | +| `PkceUnsupported` | Authorization-server metadata does not advertise `S256`. | +| `InvalidBearerChallenge` | A `WWW-Authenticate` Bearer challenge is malformed or ambiguous. | +| `InvalidBearerToken` | A token is empty, unsafe for an HTTP header, malformed, or conflicts with a configured token provider. | +| `InvalidOrigin` | A present browser Origin is malformed, duplicated, or outside the exact allowlist. | +| `InvalidRequestHost` | A required Host is absent, malformed, duplicated, or does not match deployment policy. | +| `InsecureHttpTransport` | A protected server request arrived over cleartext without an explicit development opt-in. | + ## Provider errors All adapters use the stable `ProviderRequestError` categories. diff --git a/docs/mcp-conformance.md b/docs/mcp-conformance.md index c36a2b3..7305f91 100644 --- a/docs/mcp-conformance.md +++ b/docs/mcp-conformance.md @@ -31,7 +31,7 @@ API. | MRTR | Elicitation schemas/results, roots, sampling messages/tools, declared client capabilities, and bounded retries | | Pagination | Cursor encoding for all list methods; tool discovery rejects cursor cycles and honors `max_pages` | | Subscriptions | Correlation IDs, acknowledgement-first ordering, requested filters, SSE, stdio, and cancellation | -| HTTP | Routing headers, tool-argument headers, reserved MCP error status mappings, response limits, and URL policy | +| HTTP | Routing headers, tool-argument headers, reserved MCP error status mappings, response limits, URL policy, OAuth discovery, bounded Bearer refresh/step-up, and server deployment guards | | Extensions | Mandatory prefixed identifiers, object settings, lossless unknown JSON, generic methods, and `extensionSettings` | | Compatibility | Modern/legacy classification for stdio and HTTP; actionable rejection of legacy `initialize` | @@ -56,3 +56,12 @@ state or initiate fallback. - Event JSON is borrowed for the duration of the callback. - `max_round_trips` bounds MRTR retries; `max_pages` bounds tool discovery. - MCP documents use the shared bounded JSON parser and message byte limit. +- Optional HTTP authorization follows the `2026-07-28` OAuth profile: RFC 9728 + protected-resource discovery, RFC 8414/OIDC issuer discovery, exact RFC 9207 + issuer checks, `S256` PKCE capability, RFC 8707 resource indicators, and + bounded 401/403 retries. OAuth remains disabled unless policy callbacks are + configured; stdio credentials remain an environment/application concern. +- Protected HTTP servers can reject cleartext, duplicate or untrusted Origin + headers, and Host mismatches before parsing JSON-RPC. Bearer validators receive + the token, canonical audience, method, and params; MCP handlers never receive + the credential. diff --git a/docs/security.md b/docs/security.md index 253b909..9265777 100644 --- a/docs/security.md +++ b/docs/security.md @@ -46,6 +46,47 @@ private address. For hostile inputs, combine an exact host allowlist with network egress controls or a transport whose resolver validates every resolved address. +## MCP HTTP authorization + +MCP authorization is optional. When enabled, ZigAI follows the +[`2026-07-28` authorization profile](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) +at the transport boundary: + +- `protectedResourceDiscoveryUrls` returns the endpoint-path and root RFC 9728 + URLs in required order; `parseProtectedResourceMetadata` accepts extensions + but requires the canonical resource, at least one distinct issuer, and the + Bearer header method; +- `authorizationServerDiscoveryUrls` returns RFC 8414 and OIDC URLs in required + path-aware order; parsed metadata must match the selected issuer exactly, + use eligible HTTPS endpoints, and advertise `S256` PKCE; +- `ClientPolicy` binds one canonical resource and authorization-server issuer + to a `TokenProvider`. The callback receives the RFC 8707 `resource`, method, + accumulated scopes, and refresh reason. Return tokens with + `AccessToken.initAlloc` so partial allocation failures are cleaned safely; +- Streamable HTTP adds `Authorization: Bearer ...` to every POST. A valid 401 + challenge can request a bounded refresh; a 403 `insufficient_scope` challenge + can request a bounded stable-order scope union. Static Authorization headers + cannot be combined with a token policy; +- `ServerPolicy` passes only the token value, canonical audience, method, and + params to the application verifier. The verifier must validate signature, + expiry, issuer, and audience before returning `authorized`. Denials produce + an owned `WWW-Authenticate` response header; callback descriptions and token + bytes are never reflected. + +Browser and network checks are separate in `DeploymentPolicy`. It rejects +cleartext unless explicitly enabled, requires an exact allowed Origin whenever +the header is present, and can require an exact Host value. Duplicate Origin, +Host, and Authorization headers fail closed. This implements the +[Streamable HTTP Origin requirement](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) +before JSON parsing. Bind local servers to loopback; use a reverse proxy or +host framework that supplies truthful `HttpMetadata.is_tls`, preserves response +headers, and validates resolved addresses at the network boundary. + +`ServerResponse.headers` is borrowed until `deinit`; an HTTP host must copy or +write those headers first. The standard outbound HTTP transport does not follow +redirects, so Bearer credentials cannot cross origins implicitly. Custom HTTP +transports must preserve that rule and mark Authorization headers sensitive. + ## Credentials and diagnostics Provider clients place API keys only in authentication headers. Standard names @@ -73,6 +114,8 @@ reports that suppression. | Tool arguments and results | Untrusted data | JSON, size, timeout, queue, and concurrency limits apply. Tool implementations are trusted code and must authorize side effects. | | Provider and MCP endpoints | Operator configuration | URL policy runs before callbacks and socket work. Prefer exact host allowlists. | | MCP servers and discovered tools | Untrusted peers | Protocol, capability, schema, pagination, round-trip, and JSON limits apply. Review tool descriptions and requested actions before granting authority. | +| MCP OAuth metadata and challenges | Untrusted network data | Bounded parsers, URL policy, exact issuer/resource binding, PKCE checks, and retry limits apply. Token storage and interactive authorization remain application-owned. | +| MCP HTTP host | Trusted deployment boundary | Must provide truthful headers/TLS state, emit owned response headers before cleanup, bind local services to loopback, and enforce resolver/egress policy. | | MCP input handlers and extensions | Trusted application policy over untrusted JSON | MRTR requests and responses are schema-checked, but the callback decides whether elicitation, roots, or sampling is authorized. Extension settings remain application-defined. | | Provider-managed files | Provider-scoped handles | A provider guard is checked before network I/O; do not reuse opaque handles across providers. | | Persisted history and paused state | Untrusted input | Versioned parsers and allocation limits apply. Store it with application-appropriate access control and retention. | diff --git a/src/mcp.zig b/src/mcp.zig index e64cb1e..a1e8a18 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -130,6 +130,28 @@ pub fn extensionSettings( /// MCP protocol and transport failures defined by ZigAI. pub const Error = error{ + /// An OAuth token, response, or metadata document names another issuer. + InvalidAuthorizationIssuer, + /// OAuth/OIDC discovery metadata is malformed or unsafe. + InvalidAuthorizationServerMetadata, + /// A Bearer challenge is malformed or ambiguous. + InvalidBearerChallenge, + /// A Bearer token is malformed, unsafe, or conflicts with token policy. + InvalidBearerToken, + /// A browser Origin is malformed or outside deployment policy. + InvalidOrigin, + /// RFC 9728 protected-resource metadata is malformed or mismatched. + InvalidProtectedResourceMetadata, + /// A required request Host is absent, malformed, or mismatched. + InvalidRequestHost, + /// The canonical OAuth resource URI is invalid or differs from the endpoint. + InvalidResourceUri, + /// Protected MCP HTTP was attempted over unapproved cleartext. + InsecureHttpTransport, + /// RFC 9207 issuer output was required but absent. + MissingAuthorizationIssuer, + /// Authorization-server metadata does not advertise S256 PKCE. + PkceUnsupported, /// A stdio transport was configured without a program command. EmptyCommand, /// An MCP header value does not match its schema annotation. -- knotmirror 0.0.1