Skip to content

The host is an MCP-conformant OAuth 2.1 authorization server: RFC 8414 metadata, a standard authorize endpoint, resource indicators, and Claude's Client ID Metadata Document as a client #1003

Closed

Description

@antoinetoussaint-byte

What we are trying to do

A person connects an MCP client (Claude Code, Claude Desktop, Claude.ai connectors) to a solution's MCP endpoint and signs in to the host once, in the browser, exactly as they sign in to the page — no copied tokens, no API keys standing in for a person. The first resource is the wiki (https://github.com/obin-ai/core-solutions/issues/343); the runtime half is codefly-dev/solution-runtime-go#46. Owner's direction, 2026-10-03: the SOTA, not a shortcut.

What already exists (read from v0.0.89)

The host IS an authorization server for its registered first-party public clients (handbook decisions/registered-clients.md: "every future client — Slack, CLI, mobile… is a row in the registry"): authorization code + PKCE S256, POST /v1/auth/clients/validate, POST /v1/auth/clients/authorize (issues the one-time code from a signed-in session), POST /v1/auth/token (authorization_code and refresh_token, rotating), IDENTITY_REGISTERED_CLIENTS as operator config with exact redirect URIs and loopback http allowed, JWKS at /v1/auth/.well-known/jwks.json, tokens carrying azp stamped as x-client-id (accounts authentication.proto:196-300, 499-560; pkg/auth/clients.go; ext_authz.go:320-325). obin-word is the first consumer of that flow. What an MCP client needs is the same server, speaking the standard discovery the MCP spec (2025-11-25 / 2026-07-28) requires.

What does not exist: RFC 8414 authorization-server metadata; a standard /authorize endpoint (the login page plus /v1/auth/clients/authorize play that role); RFC 8707 resource → token audience; Client ID Metadata Documents (RFC draft, the MCP-preferred registration; Claude Code's is https://claude.ai/oauth/claude-code-client-metadata, redirect URIs http://localhost/callback and http://127.0.0.1/callback on any port); dynamic client registration (RFC 7591) — deprecated by MCP 2026-07-28 in favour of CIMD and NOT wanted here (unbounded registry growth, impersonation); OIDC discovery for the host's own issuer.

What to build (one PR, with the handbook decision page updated in the same change set)

  1. Authorization-server metadata at /.well-known/oauth-authorization-server (RFC 8414): issuer, authorization_endpoint, token_endpoint, jwks_uri, code_challenge_methods_supported: ["S256"], grant_types_supported: ["authorization_code","refresh_token"], token_endpoint_auth_methods_supported: ["none"], client_id_metadata_document_supported: true, scopes_supported, response_types_supported: ["code"]. No registration_endpoint (no DCR).
  2. A standard GET /oauth2/authorize that validates client_id, redirect_uri, code_challenge (+ method), state, resource (RFC 8707), scope, then sends the person to the existing login page carrying those parameters and, once signed in, shows consent naming the client (from its metadata) and the resource (the solution), and completes through the existing clients/authorize → redirect with code + state. POST /v1/auth/token (or a /oauth2/token alias) accepts resource and binds the token's audience to it.
  3. Client ID Metadata Documents: a client_id that is an https URL is fetched (bounded size, timeout, cache with the document's TTL, https only, no redirects to another origin), validated (client_id in the document equals its URL; redirect_uris must contain the presented one exactly, loopback http allowed per the existing rule; token_endpoint_auth_method: none), and treated as a public client of the registry for that flow — nothing durable is written. Operator-registered rows keep working unchanged. Decide and record: whether CIMD clients are allowed for every organisation or allow-listed per org (the registered-clients decision page says who decides).
  4. Audience at the solution: a token minted with resource = https://<host>/solutions/wiki/mcp carries that audience; the gateway, when it fronts /solutions/<id>/mcp, accepts a token whose audience is that resource (or the session-kind token it accepts today) and refuses one minted for another resource. The gateway keeps stamping the same identity headers, so the runtime (ServeMCP: a solution exposes an MCP server as the signed-in person — Streamable HTTP, RFC 9728 metadata, tools that run under the viewer's Work Context solution-runtime-go#46) changes nothing for auth.
  5. Session semantics: an MCP token is a session credential (x-credential-kind=session, carries a session id) so the solution SDK can mint the viewer's Work Context; its lifetime, refresh rotation and revocation follow the registered-client tokens.
  6. Handbook: decisions/registered-clients.md gains the CIMD rule and the "no DCR" reason; design/integration-surfaces.md's open question "who owns the MCP server" is answered: the solution owns its tool surface, the runtime serves it, the host authenticates — the gateway has no MCP code.
  7. Stories/tests: the authorize → consent → token → gateway path for a CIMD client with a loopback redirect; a token for resource A refused at resource B; a CIMD document that fails each validation rule refused by name; metadata document conformance (the MCP client discovery chain: 401 → PRM → AS metadata).

Done when

claude mcp add --transport http wiki https://<host>/solutions/wiki/mcp followed by /mcp → browser sign-in (WorkOS) → consent → tools work, with no --header, on staging; Claude Desktop's connector works the same way (CIMD). The decision page says so.

Activity

  1. antoinetoussaint-byte commented on Oct 4, 2026

    @antoinetoussaint-byte
    ContributorAuthor

    Two facts from the executed review of the runtime half (codefly-dev/solution-runtime-go#47), both this issue's to absorb:

    1. The 401 challenge is the gateway's, not the runtime's. For /solutions/<id>/* the gateway strips identity, runs ext_authz, and on a missing bearer answers 401 authentication required itself (gateway_solutions.go:143-158 → ext_authz.go:162 → gateway.go:516) — it never proxies, so the runtime's WWW-Authenticate: Bearer resource_metadata="…" challenge can only ever be seen by a direct-to-port caller. An MCP client's discovery chain (401 → Protected Resource Metadata → AS metadata) therefore starts at the gateway: a denied 401 on /solutions/<id>/* must carry WWW-Authenticate: Bearer resource_metadata="<origin>/solutions/<id>/.well-known/oauth-protected-resource", derivable from the route; the .well-known GET is already public on that route (gateway_solutions.go:201). Add it to item 4 of this issue.

    2. The issuer is a literal today. The gateway verifies host JWTs against the literal issuer "saas-starter" (ext_authz.go:123) and serves no authorization-server metadata; the runtime's RFC 9728 document names authorization_servers from HOST_ISSUER_URL (defaulting to the resolved frontend origin). The RFC 8414 document this issue adds must be served at that origin's /.well-known/oauth-authorization-server with issuer equal to what tokens carry — so either the iss claim becomes the host's https origin, or the metadata is served under the literal's URL form. Decide and state it in the PR.

  2. 2 remaining items

  3. added a commit that references this issue on Oct 6, 2026
    056803f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions