Repository navigation
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
Description
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)
- 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"]. Noregistration_endpoint(no DCR). - A standard
GET /oauth2/authorizethat validatesclient_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 existingclients/authorize→ redirect withcode+state.POST /v1/auth/token(or a/oauth2/tokenalias) acceptsresourceand binds the token's audience to it. - Client ID Metadata Documents: a
client_idthat is an https URL is fetched (bounded size, timeout, cache with the document's TTL, https only, no redirects to another origin), validated (client_idin the document equals its URL;redirect_urismust 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). - Audience at the solution: a token minted with
resource = https://<host>/solutions/wiki/mcpcarries 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. - 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. - Handbook:
decisions/registered-clients.mdgains 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. - 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
antoinetoussaint-byte commentedon Oct 4, 2026
Two facts from the executed review of the runtime half (codefly-dev/solution-runtime-go#47), both this issue's to absorb:
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 answers401 authentication requireditself (gateway_solutions.go:143-158→ext_authz.go:162→gateway.go:516) — it never proxies, so the runtime'sWWW-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 carryWWW-Authenticate: Bearer resource_metadata="<origin>/solutions/<id>/.well-known/oauth-protected-resource", derivable from the route; the.well-knownGET is already public on that route (gateway_solutions.go:201). Add it to item 4 of this issue.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 namesauthorization_serversfromHOST_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-serverwithissuerequal to what tokens carry — so either theissclaim becomes the host's https origin, or the metadata is served under the literal's URL form. Decide and state it in the PR.fix: refuse a deployed MCP surface nobody addressed, and say who sees…
feat: ServeMCP — a solution exposes its tools as the signed-in person (…
fix: scrub three solution-named placeholders the boundary gate found …
fix: an impersonation test that tested no impersonation, and a refres…
fix: the Astra review's blocker and six more, with the reproducers ad…
fix: the resource is the solution proxy, the path a client can actual…
fix: a solution's tool endpoint admits only a token issued for itself…
fix: renumber the OAuth resource migration to 18, behind main's 17 (R…
fix: three audience outcomes, a fail-closed sign-in state, and the §7…
docs: the three audience readings, the fail-closed state, and the §7.…
test: record the cross-repository resource contract where a change tr…
fix: the resource identifier is compared by code-point equality (Refs #…
2 remaining items
fix(auth-gateway): a module or solution upstream receives the person'…
docs: name the test that catches a host route change
fix: close the eight self-review findings, at the authority in each c…
fix: classify a loopback address in every spelling a resolver accepts
chore: remediate two freshly published high-severity advisories (Refs #…
fix: read a URL's host the way the client that dials it does
fix: a host is an address, a name, or neither — and neither is a refusal
fix: a host that maps cleanly can still be no name at all
The host is an MCP-conformant OAuth 2.1 authorization server: RFC 841…
test: defend the digit validator's rejection of a non-digit character
fix(auth-gateway): a module or solution upstream receives the person'…
fix(auth-gateway): a module or solution upstream receives the person'…
fix: one APP_BASE_URL declaration, and a gate that names a duplicated…
Security posture audit 2026-10-04: the 21 findings this repo owns (Cl…