Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,10 @@ Endpoints:
- `GET /.well-known/oauth-protected-resource` — points clients at the AS
- `POST /oauth/register` — dynamic client registration (RFC 7591); `redirect_uris`
are stored and persisted to `OAUTH_CLIENTS_FILE`; requested `grant_types` are
honoured (intersected with `authorization_code`)
honoured (intersected with `authorization_code`). A **hosted** `redirect_uri` is
rejected unless its host is on the allow-list (see the Companion-control note
below); loopback redirects are always accepted.

- `GET /oauth/authorize` — validates the client + redirect, requires PKCE, sets
the binding cookie, then redirects to II's handshake (with `registration_key`)
- `GET /oauth/connect/callback` — the **pinned callback page**: II navigates here
Expand Down Expand Up @@ -408,12 +411,21 @@ incl. loopback** (a loopback redirect resolves on the consenter's own machine).
`SameSite=Lax` still rides the top-level cross-site GET II uses to navigate back to
the callback page.

> **Companion control (not in this change).** The *same-browser* variant — a victim
> socially engineered into running the whole flow toward an attacker-registered
> **hosted** `redirect_uri` — is not closed by this (the victim's browser
> legitimately holds both proofs). It needs **hosted-redirect allow-listing**;
> loopback/native clients are safe either way (the code resolves on the consenter's
> own machine).
> **Companion control: the hosted-redirect allow-list.** The *same-browser* variant
> (a victim socially engineered into running the whole flow toward an
> attacker-registered **hosted** `redirect_uri`) is not closed by Consent-Bound
> Completion alone, since the victim's browser legitimately holds both proofs. It
> **is** closed by a **hosted-redirect allow-list**: dynamic client registration
> accepts only a loopback redirect, or a hosted redirect whose host is (a subdomain
> of) an allow-listed registrable domain, so an attacker cannot register a hosted
> destination it controls. The list is seeded with the known MCP connector vendors
> and widened per deployment with `OAUTH_ALLOWED_REDIRECT_DOMAINS` (additive);
> loopback/native clients are exempt (the code resolves on the consenter's own
> machine). Enforced at both `/oauth/register` and `/oauth/authorize`. A client
> turned away is pointed at a contact address to request approval:
> `/oauth/register` says so in its JSON `error_description`, and a browser that
> reaches `/oauth/authorize` gets an on-brand "not allowed" page (not a raw error)
> naming the same contact.

### The registration-delegation connect handshake

Expand Down Expand Up @@ -700,8 +712,8 @@ delegation. Omitting `account` uses the default account.
handshake binds the connection's session key (a fragment-delivered, canister-signed
delegation redeemed via `mcp_register_v2`), with **Consent-Bound Completion** binding
`/oauth/connect/redeem` to both the initiator (`sid` cookie) and the consenter (the
fragment delegation); expiring tokens. (The RFC 8628 device grant was dropped.
Same-browser-variant closure needs hosted-redirect allow-listing — see Auth.)
fragment delegation); expiring tokens. The same-browser phishing variant is closed
by a hosted-redirect allow-list (see Auth). (The RFC 8628 device grant was dropped.)
- [x] On-demand **domain identities**: the registered session key mints per-app
account delegations directly via II canister methods
(`call_canister`/`get_app_principal` `derivation_origin`); no per-app browser flow.
Expand Down
13 changes: 13 additions & 0 deletions src/assets/connect-error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>MCP client not allowed</title>
<style nonce="__NONCE__">__CSS__</style></head>
<body><main class="screen error">
<div class=stage>
<h1 class=msg>This MCP client isn't allowed</h1>
<p class=detail>This server only accepts connections from approved MCP clients, and the one you're connecting from is not on the allow-list.</p>
<p class=hint>To request access, email <a href="mailto:__CONTACT__">__CONTACT__</a> and include your MCP client's redirect URI.</p>
</div>
<footer class=hosted><span class=hosted-label>Hosted by</span>
<span class=hosted-logo>__LOGO__</span></footer></main>
</body></html>
5 changes: 4 additions & 1 deletion src/assets/connect.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ body {
color: var(--ink);
}
.detail { margin: 0; max-width: 32ch; font-size: 1rem; color: var(--secondary); }
.hint { margin: 0; font-size: 0.9rem; color: var(--muted); }
.hint { margin: 0; max-width: 34ch; font-size: 0.9rem; color: var(--muted); }
/* Links appear only on the error page (the callback page has none): keep them on
the Ember accent so they read as links in both themes without a UA blue. */
.detail a, .hint a { color: var(--accent); }
/* Foot-of-page attribution: an eyebrow label + the DFINITY mark in a soft pill. */
.hosted {
display: flex;
Expand Down
Loading