Skip to content

Add ICRC-167: Browser URL Transport draft standard - #246

Merged
sea-snake merged 9 commits into
mainfrom
icrc-167-browser-url-transport
Sep 10, 2026
Merged

sea-snake merged 9 commits into
mainfrom
icrc-167-browser-url-transport

Conversation

@sea-snake

@sea-snake sea-snake commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator

Promotes the Browser URL Transport entry (previously ![IDEA]) to a draft transport standard, as ICRC-167.

What it defines

An ICRC-25 transport channel based on browser URL navigation, following the same document shape as ICRC-29 and ICRC-94:

  • The relying party navigates the browser to the signer with the JSON-RPC request in the URL; the signer navigates back to a relying-party callback URL with the JSON-RPC response in the URL.
  • Messages are carried in the URL hash fragment, which is never sent to the server — so canister-call arguments, delegations and accounts don't leak into any backend logs. Query/path use is ruled out for message content.
  • Fills the gap where window.postMessage (ICRC-29) isn't available: top-level redirects between two web pages and native/mobile deep links.
  • This is effectively the pattern Internet Identity's MCP connect flow already uses, generalized to carry ICRC-25 JSON-RPC.

Interaction models

  • Single request/response — one navigation each way.
  • Batched requests — message may be a JSON-RPC 2.0 batch, so a relying party requests e.g. a delegation (ICRC-34) and accounts/attributes in a single navigation (parallel).
  • Multi-step — a logical interaction may be several sequential round-trips when a later request depends on an earlier response; the signer's per-origin permission state means no re-consent between hops.
  • Signer-initiated — a signer may start a flow (e.g. push a delegation) by navigating to one of the relying party's declared callbacks with init in place of a message, prompting the relying party to begin an ordinary request flow (modelled on OIDC third-party-initiated login). An optional signer hint lets it preselect which of the relying party's known signers to use. init is a pure trigger and the hint can only select among already-trusted signers, so the relying party never navigates to an untrusted destination. The declared callbacks double as init entry points, so no second endpoint is introduced.

Safety design

  • Callback allow-list. The callback is attacker-craftable, so the signer honors it only when it exact-matches an entry the relying party declares at /.well-known/ii-auth-callbacks (reusing II's allow-list, strict fail-closed fetch: no redirects, no credentials, no-store, application/json, size cap, same-origin entries, no fragments, CORS). The declared callbacks are the complete set of URLs a signer may navigate the browser to for that RP. A state param the signer echoes back binds a response to the initiating request (the callback itself must stay static to match the allow-list).
  • Verified deep links only. Android App Links / iOS Universal Links, which the OS binds to an app only after verifying domain ownership. Bare custom URI schemes are forbidden for response delivery — any app can register one and intercept a returned delegation, and a scheme has no origin to validate the allow-list against.

Changes

  • New topics/icrc_167_browser_url_transport.md.
  • README.md: Transport Channel table entry updated from ![IDEA] to ![DRAFT] with a link.

Companion registry issue: dfinity/ICRC#167

sea-snake and others added 6 commits July 15, 2026 17:30
Promotes the "Browser URL Transport" idea to a draft transport standard.
Defines an ICRC-25 transport channel over browser URL navigation, carrying
JSON-RPC messages in the URL hash fragment so message contents are never
sent to a backend. Suitable for top-level redirects and native/mobile deep
links where a window.postMessage channel (ICRC-29) is unavailable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address working-group feedback:
- Require domain-verified deep links (App Links / Universal Links) and
  forbid custom URI schemes, which any app can register and use to
  intercept a returned delegation.
- Allow the request to be a JSON-RPC 2.0 batch so a relying party can
  request e.g. a delegation and accounts in a single navigation.
- Validate the callback against a relying-party-declared allow-list at
  /.well-known/ii-auth-callbacks (exact match, strict fail-closed fetch)
  instead of accepting an arbitrary callback, and add a `state` param the
  signer echoes back to bind the response to the initiating request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address further working-group feedback:
- Document that an interaction may span several sequential round-trips
  (a later request depending on an earlier response), driven by the
  relying party, with signer per-origin permission state avoiding
  re-establishment between hops.
- Add signer-initiated interactions: a signer navigates to one of the
  relying party's declared callbacks with `init` + `transport` in place
  of a `message`, prompting the relying party to begin an ordinary
  request flow (modelled on OIDC third-party-initiated login). The
  declared callbacks double as init entry points, so no second endpoint
  or well-known key is introduced. The relying party must validate the
  untrusted `transport` before navigating to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The signer that initiates is reached over this same URL transport, so a
`transport` param is redundant and would be an attacker-suppliable
destination. Remove it: `init` is now a pure trigger, and the relying
party determines the signer exactly as for any relying-party-initiated
interaction, so it never navigates to a signer chosen by untrusted input.
Drop the now-unneeded untrusted-transport validation and error case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a signer-initiated first hop, a relying party that supports multiple
signers otherwise has to prompt the user to pick one. Add an optional
`signer` hint (the signer's origin) so the initiating signer can
preselect the intended one. Unlike the removed `transport`, the hint only
selects among signers the relying party already knows: an unrecognised
value is ignored and the relying party never navigates to a signer solely
because the init navigation named it, so the untrusted-destination
property is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expand the Deep Links section with concrete setup steps and official
documentation links for the platforms where a browser-to-app hand-off is
reliable (iOS Universal Links, Android App Links).

Correct the desktop story: verified browser-to-native-app hand-off does
not work there. macOS opens Universal Links in the browser by default,
and on modern Windows a supported link opened in a modern browser stays
in the browser (only the deprecated Edge Legacy launched the app; the
only browser-firing option is a forbidden custom protocol). Recommend the
browser transport or a developer-implemented loopback (127.0.0.1)
callback (RFC 8252) for native desktop apps, noting loopback does not fit
the origin-based callback allow-list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sea-snake and others added 3 commits July 17, 2026 18:12
Loading a declared callback with no `message` is already an ordinary
entry into the relying party's flow, so a signer initiates simply by
navigating the user to a declared callback — no `init` marker or `signer`
hint is needed. Simplify the Signer-Initiated Interaction section and its
cross-references accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A native desktop app reaches a signer by opening a web page it controls
that acts as an ordinary relying party over any applicable transport, and
forwarding the messages to the app over a loopback (127.0.0.1) server
(RFC 8252). The page's callback is its own https URL, so the callback
allow-list applies normally; the loopback is only the page-to-app bridge,
not a signer callback. Drop the irrelevant ICRC-29 suggestion and the
incorrect "loopback callback doesn't fit the allow-list" framing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k steps)

Replace the vague desktop paragraph with a concrete three-step pattern:
the native app opens a web page it controls, that page runs the flow as
an ordinary relying party, and it relays the result to the app over a
local channel (e.g. a loopback server). State plainly that only the web
page participates in the transport and the relay is out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake merged commit 202e6df into main Sep 10, 2026
5 checks passed
@sea-snake
sea-snake deleted the icrc-167-browser-url-transport branch September 10, 2026 11:18
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.

2 participants