docs: scope client-branding extension (product name + logo to id.ai) - #103
docs: scope client-branding extension (product name + logo to id.ai)#103aterga wants to merge 2 commits into
Conversation
A scoping doc for surfacing the requesting MCP client's product identity to Internet Identity, so the id.ai consent screen can show which vetted product is connecting (a name and a logo) instead of an anonymous bridge. The design: - The MCP server puts a curated product-name slug in the II connect-link fragment, and serves the matching name and logo from two GET endpoints on the same origin II already validates for the #4091 callback check. - Branding is shown for vetted products only; the name and logo are curated server-side and never taken from client-supplied DCR metadata (open DCR is unauthenticated, so rendering client strings would be a consent-phishing vector). - Nothing keys on the OAuth client_id: those are per-registration UUIDs that churn on reinstall, expiry, and LRU eviction. Product identity is derived from the vetted redirect vendor instead (the hosted-redirect allow-list), which is stable and self-authenticating. - v1 covers self-authenticating web connectors only. Native/desktop apps authenticate over loopback (no verifiable identity), so they stay anonymous until a signed software_statement path lands (deferred). Docs-only; requires a coordinated counterpart change on the id.ai side, which the doc specifies as the boundary contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Adds a scoping/design document defining a proposed “client branding” extension for MCP→Internet Identity authorization, allowing id.ai to show a vetted connector’s product name and logo on the consent screen by using a curated server-side product slug and same-origin endpoints.
Changes:
- Add
docs/scoping-client-branding.mddescribing the proposed contract/flow, vetting model, endpoints, security considerations, and phased rollout plan.
Comments suppressed due to low confidence (4)
docs/scoping-client-branding.md:55
- The
POST /oauth/registercitation is off: the handler isregisteratsrc/auth.rs:1897(the current1888line is insidegranted_grant_types). Please update the reference to match the actual endpoint implementation.
Dynamic client registration is **unauthenticated and open** (`POST /oauth/register` takes all
callers, `auth.rs:1888`). So any client-supplied `client_name` / `logo_uri` is attacker-
controlled. Rendering it on a consent screen would be a phishing gift: a hostile client
docs/scoping-client-branding.md:70
- The
include_str!reference for the inlined DFINITY logo points toauth.rs:1161, but the logo include is currentlyCONNECT_LOGO_SVGatsrc/auth.rs:1170(1161 is in the preceding doc/comment block). Updating the citation will keep the “verified” claim accurate.
client `logo_uri`. Logos are compiled-in assets (like the DFINITY logo already inlined into the
callback page via `include_str!`, `auth.rs:1161`).
docs/scoping-client-branding.md:77
- The
client_idminting citation appears to be pointing at the invalid-redirect error branch (src/auth.rs:1913), not where theclient-{uuid_v4}value is created (src/auth.rs:1922). Updating the line reference will prevent confusion when someone goes to verify the claim.
- Minted fresh per registration: `client-{uuid_v4}` (`auth.rs:1913`), a new random value each
time anyone registers.
docs/scoping-client-branding.md:185
- This section says the
/brandingresponses are “cacheable”, but earlier the metadata endpoint is specified asno-store(and later you reiterateno-storefor the JSON). Consider clarifying that only the logo bytes are intended to be cacheable, while the JSON metadata should beno-store(or adjust the endpoint contract if you intend caching for both).
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
is a `404`. Responses are small, cacheable, and served from compiled-in assets.
- **Logo rendering on II (the one II-side must).** SVG is acceptable as the logo format, but only
if II renders it through a fixed-size `<img src=...>`. It must never inline server-returned SVG
into the consent DOM: inlined SVG can execute script, an `<img>`-loaded SVG cannot. (A raster
logo would sidestep the concern entirely, but `<img>`-rendered SVG is safe and keeps the crisp
vector mark.)
- **CORS / caching** mirror the `#4091` well-known: `Access-Control-Allow-Origin` for the JSON
metadata (II uses `fetch`), and `no-store` so an intermediary cannot serve a mapping stale after
a curation change.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot noted the "verified, file:line" citations had drifted from src/auth.rs. The doc's numbers were taken from the pre-merge #98 branch; after #98 (the MCP05 redirect fix) merged, everything below redirect_uri_permitted shifted by about nine lines. Re-synced every citation against current main and verified each against the source: authorize 865 -> 874 validate_client 890 (call) -> 759 (definition) #4091 well-known 1082-1109 -> 1091-1118 error copy 923 -> 932 ii_mcp_url 1062 -> 1071 connect_callback_url 1088 -> 1097 auth_callbacks 1097 -> 1106 logo include_str! 1161 -> 1170 register 1888 -> 1897 client-{uuid} 1913 -> 1922 Unchanged (above the shift): make_room_for_client (340), DEFAULT_ALLOWED_REDIRECTS (399), the path-pin comment (383-391), and the loopback-exempt note (393). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
docs/scoping-client-branding.md:20
- This section says “Two decisions frame everything below,” but it lists three bullets. Update the count to match to avoid confusion in the scoping doc.
Two decisions frame everything below:
docs/scoping-client-branding.md:177
- This bullet says responses are “cacheable,” but a few lines later the doc specifies
no-storefor the JSON metadata. Clarify which endpoint(s) are intended to be cacheable vs explicitly non-cacheable so implementers don’t pick conflicting headers.
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
is a `404`. Responses are small, cacheable, and served from compiled-in assets.
aterga
left a comment
There was a problem hiding this comment.
Review from the II-side design point of view
Answering §10, since it's addressed to the II team. Grounded in the current II frontend/backend (the id.ai/mcp connect flow, the declared-callback allow-list, and the production CSP).
The identity/trust model is right — no changes needed there
- Curated, server-side product identity, never client-supplied. Correct. II isn't a party to the client↔server OAuth, so anything the client can set (DCR
client_name,logo_uri) is unauthenticated and phishable. Deriving identity from the vetted redirect vendor is a sound proxy: II's declared-callback allow-list (fetch of/.well-known/ii-auth-callbacks, fail-closed, exact match) already means a connect can only reach us over an origin that published that manifest, so the vendor is effectively attested by the same mechanism. - Product slug, not
client_id. Correct. The connect link is attacker-craftable andclient-{uuid_v4}churns / is LRU-evicted, so aclient_idin the fragment would be a spoofable, unstable display key. A closed slug space resolved server-side is the right identifier. - Additive / backward-compatible contract. Confirmed on our end — the link parser ignores unknown fragment params today, so an added
connectoris safe for old II, and old servers simply omit it. The only II code cost is one field added to the link schema.
The one real gap: logo transport will hit II's CSP (§10 Q2)
The doc's model — II renders '<img src="{issuer}/branding/{slug}/logo">' pointing at the callback origin — is blocked by II's Content-Security-Policy. This is the main thing to revise.
II's production CSP is:
img-src 'self' data: https://*.googleusercontent.com;
connect-src 'self' https:;
So:
- A cross-origin
'<img src="https://<callback-origin>/…/logo">'won't load —img-srcallows only'self',data:, and one fixed Google-avatar host. We don't want to widenimg-srctohttps:, since that reopens exactly the cross-origin image surface we keep closed. fetch()to any HTTPS origin is allowed (connect-src 'self' https:).
Recommended contract instead: II fetch()es the branding from the trust-confirmed callback origin and renders the logo as a data: URI (already permitted by img-src data:, no CSP change). Simplest shape: serve one metadata document with the logo inlined as a bounded base64 data: image, so II makes a single request to a single origin and never needs a second cross-origin image hop. That also answers §10 Q4 — one doc with an inline logo beats separate name/logo requests for us.
Note this flips one assumption in the doc: "No CORS needed for <img>" doesn't apply, because we reach the logo via fetch() (which does need the CORS header you already specify) and then render data:.
SVG vs raster (§10 Q2 cont.)
The doc's point that an <img>-loaded SVG can't execute script is technically true. But once II is building a data: URI from fetched bytes, I'd steer away from SVG: it keeps a latent "never inline this" invariant that future contributors won't know, whereas raster (PNG) is strictly safer and removes the question entirely. Recommend the curated logos be PNG, with II applying a raster-only content-type allow-list, a byte-size cap, a decode step, and a fail-closed fallback to the anonymous icon on any validation failure — i.e. apply the same hardening the doc reserves for a hypothetical Phase-3 open path, in v1, because II is the side putting bytes on screen.
Trust ordering — please state it explicitly
connector={slug} lives in the fragment, which is attacker-craftable (a malicious link can claim &connector=claude). Safety comes entirely from where II fetches branding: only from the callback origin, and only after that origin is trust-confirmed — i.e. after the certified trusted_url gate confirms the server origin, and after the callback is matched against the origin's declared allow-list. The slug is display-only routing; the authority for "is this really ChatGPT" is that the branding came from an origin II independently trust-confirmed, never the fragment's claimed value. Worth spelling out so the II implementation doesn't fetch branding on the honest-untrusted / unverified path.
§10, point by point
- Fragment param + endpoint paths —
connector={slug}is fine (additive; parser ignores unknowns). Prefer a single metadata endpoint with an inline logo over a separate/logoroute. - Render logo via fixed-size
'<img src=…>'? Not cross-origin (CSP blocks it). Yes viafetch()+data:, or an inline base64data:logo in the JSON — and please make it raster, not SVG. - "Verified connector" treatment + name delivery — deliver the name in the JSON, not mirrored in the fragment (fragment is untrusted; JSON comes from the trust-confirmed origin). The exact verified-vs-anonymous consent UI is ours to specify and I'll take it to the II design owners; the security contract we need from the server is: branding is shown ⇔ trust-confirmed origin + declared callback + valid slug + valid logo; otherwise fall back to the anonymous screen.
- Single doc vs. separate requests — single metadata document with the logo inlined as base64
data:. One request, one origin, no CSP friction.
Minor
- Security section is solid: no client-supplied content, no SSRF in v1 (logos bundled at build time), closed slug space with 404 otherwise. If the Phase-3 open self-service path is ever built, the SSRF-pinned client + https-only + size/dimension caps + raster-only + decode-then-re-encode is exactly right — but the rendering hardening (size cap / raster / fail-closed) should be II-side and present from v1, not deferred.
- Keeping native/desktop (loopback-redirect) connectors anonymous until a signed
software_statementis the correct v1 cut — those are exempt from the callback allow-list, so there's no vetted-vendor anchor to derive identity from.
Bottom line: the trust model needs no changes. The one substantive revision is the logo transport — replace "II renders a cross-origin <img src>" with "II fetches from the trust-confirmed origin (or reads an inline base64 logo from the metadata JSON) and renders a data: raster image, failing closed to the anonymous screen." That's the only part that breaks on contact with II's CSP.
Generated by Claude Code
Summary
A scoping doc (
docs/scoping-client-branding.md) for an extension that surfaces the requesting MCP client's product identity to Internet Identity, so theid.aiconsent screen can show which vetted product is connecting (a name and a logo) instead of an anonymous "some bridge". Docs-only, no code. It defines the boundary contract for both sides, since the counterpart change lives onid.ai.The flow (matching the requested round-trip):
Related issues
Grew out of the client-identity discussion; no issue closed. Reuses the
#4091callback-origin trust model and the hosted-redirect allow-list already inauth.rs.Changes
docs/scoping-client-branding.md. Key design points:ii_mcp_url,auth.rs:1062) and serves the name + logo from two GET endpoints on the same origin II already validates for#4091.client_id: those are per-registration UUIDs that churn on reinstall/expiry/LRU eviction. Product identity is derived from the vetted redirect vendor (allowed_redirects()), which is stable and self-authenticating.software_statementpath lands (deferred to Phase 3).'<img src=...>', never inlined into the consent DOM.Testing
Docs-only change; no build or test impact.
cargo build --locked --all-targets(n/a, docs-only)cargo test --locked --all-targets(n/a, docs-only)Checklist
file:lineevidence against the currentauth.rs.🤖 Generated with Claude Code
Generated by Claude Code