Skip to content

docs: scope client-branding extension (product name + logo to id.ai) - #103

Open
aterga wants to merge 2 commits into
mainfrom
claude/client-branding-proposal
Open

docs: scope client-branding extension (product name + logo to id.ai)#103
aterga wants to merge 2 commits into
mainfrom
claude/client-branding-proposal

Conversation

@aterga

@aterga aterga commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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 the id.ai consent 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 on id.ai.

The flow (matching the requested round-trip):

authorize:  MCP server --302 to id.ai/mcp#...&connector=<slug>-->  id.ai
callback:   id.ai --GET {callback origin}/branding/<slug>/logo-->  MCP server
            MCP server --SVG (image/svg+xml)-->  id.ai
consent:    id.ai --consent HTML with the logo via <img>-->  User

Related issues

Grew out of the client-identity discussion; no issue closed. Reuses the #4091 callback-origin trust model and the hosted-redirect allow-list already in auth.rs.

Changes

  • Add docs/scoping-client-branding.md. Key design points:
    • MCP server puts a curated product slug in the II connect-link fragment (ii_mcp_url, auth.rs:1062) and serves the name + logo from two GET endpoints on the same origin II already validates for #4091.
    • Vetted products only: name and logo are curated server-side, never taken from client-supplied DCR metadata (open DCR is unauthenticated, so rendering client strings would be a consent-phishing vector).
    • Nothing keys on 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.
    • v1 scope = self-authenticating web connectors only (claude.ai web, ChatGPT, Cursor, Grok, Perplexity, Antigravity). Native/desktop apps authenticate over loopback (no verifiable identity), so they stay anonymous until a signed software_statement path lands (deferred to Phase 3).
    • SVG logos are fine provided II renders them via a fixed-size '&lt;img src=...&gt;', 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

  • Docs-only; the doc is itself the change, and it carries file:line evidence against the current auth.rs.
  • No secrets, credentials, or internal-only information are included.

🤖 Generated with Claude Code


Generated by Claude Code

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md describing 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/register citation is off: the handler is register at src/auth.rs:1897 (the current 1888 line is inside granted_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 to auth.rs:1161, but the logo include is currently CONNECT_LOGO_SVG at src/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_id minting citation appears to be pointing at the invalid-redirect error branch (src/auth.rs:1913), not where the client-{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 /branding responses are “cacheable”, but earlier the metadata endpoint is specified as no-store (and later you reiterate no-store for the JSON). Consider clarifying that only the logo bytes are intended to be cacheable, while the JSON metadata should be no-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.

Comment thread docs/scoping-client-branding.md Outdated
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
@aterga
aterga marked this pull request as ready for review July 30, 2026 11:54
@aterga
aterga requested review from a team and Copilot July 30, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-store for 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 aterga left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and client-{uuid_v4} churns / is LRU-evicted, so a client_id in 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 connector is 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 '&lt;img src="{issuer}/branding/{slug}/logo"&gt;' 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 '&lt;img src="https://&lt;callback-origin&gt;/…/logo"&gt;' won't loadimg-src allows only 'self', data:, and one fixed Google-avatar host. We don't want to widen img-src to https:, 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

  1. Fragment param + endpoint pathsconnector={slug} is fine (additive; parser ignores unknowns). Prefer a single metadata endpoint with an inline logo over a separate /logo route.
  2. Render logo via fixed-size '&lt;img src=…&gt;'? Not cross-origin (CSP blocks it). Yes via fetch() + data:, or an inline base64 data: logo in the JSON — and please make it raster, not SVG.
  3. "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.
  4. Single doc vs. separate requestssingle 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_statement is 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants