Skip to content

fix(local): drop the callback allow-list II can't fetch from loopback - #172

Open
sea-snake wants to merge 1 commit into
mainfrom
fix/local-drop-dead-allowlist
Open

fix(local): drop the callback allow-list II can't fetch from loopback#172
sea-snake wants to merge 1 commit into
mainfrom
fix/local-drop-dead-allowlist

Conversation

@sea-snake

Copy link
Copy Markdown
Collaborator

Pairs with dfinity/internet-identity#4305.

Why

The local login listener serves /.well-known/ii-auth-callbacks (login/routes.rs) so II's #4091 check will honour its callback. That check can't run against a local server: browsers block II's https document from fetching a http://127.0.0.1 URL, so the request never arrives — and because the check is fail-closed, every local sign-in is refused. The route's own doc comment ("fail-closed, so serving this document is mandatory") describes something that never happens.

II now exempts a server on loopback from the allow-list and takes the callback as given. It trusts a local server by loopback host rather than by exact origin, stored port-less as http://127.0.0.1 — which is exactly what lets this listener keep binding port 0 and getting a fresh port per handshake. Standing in the check's place: the user's deliberate opt-in to a local connector in II Settings, II's notice before the first local sign-in for an identity on a machine, and its consent screen on every connect.

What changes

  • login/routes.rs — the auth_callbacks route and handler go. The handshake is two routes now: the pinned callback page and the slim redeem. RouteCtx loses callback_url (it existed only to fill the allow-list document), so login_router loses that parameter.
  • login/mod.rs — the module doc no longer cites #4091 as a reason the listener must exist; the fragment still needs a served page to read it, which is reason enough. The port-0 comment no longer claims the callback and an allow-list entry must not drift.
  • login/tests.rs — the surface test now asserts the listener serves the pinned page and nothing else, 404-ing the retired well-known along with any other path.
  • e2e_local_login.rs — drops the allow-list fetch step, and sets the anchor's trusted URL to http://127.0.0.1 (the shape a local login actually runs against) rather than an arbitrary http://localhost:8000/mcp.
  • docs + README — the scoping doc's open mixed-content/CORS questions are answered and closed; the "three loopback routes" descriptions become two; the README notes that the allow-list is a hosted-server obligation and why a local server is exempt.

The hosted server is untouched — II still enforces the allow-list for an https origin, and src/auth.rs / src/lib.rs keep serving it.

Also

Records why state is load-bearing here in a way it isn't for a hosted server. A hosted callback sits on a domain a random page can't reach; this listener is reachable by any page in the browser and any process on the machine, so an unguessable state (already a v4 UUID) is what stops a forged POST to /redeem from injecting an identity the user never chose. II treats it as opaque and only echoes it, so keeping it unpredictable is entirely this side's job.

Verification

cargo test -p imcp2-local — 21 passed. cargo check -p imcp2-local --features e2e clean. cargo fmt --check and cargo clippy -p imcp2-local --all-targets produce byte-identical output before and after this change (the tree has 518 pre-existing fmt diffs on main, 3 of them in this crate, all in server.rs and untouched here).

Merge order

This can land first — local login doesn't work against main-era II either way, since that gate rejects a loopback callback before the allow-list is ever consulted. But it only becomes useful once #4305 ships.

🤖 Generated with Claude Code

The local login listener served `/.well-known/ii-auth-callbacks` so II's
#4091 check would honour its callback. That check cannot run against a
local server: browsers block II's https document from fetching a
loopback URL, so the request never arrives and, since the check is
fail-closed, every local sign-in would be refused.

II therefore exempts a server on loopback from the allow-list and takes
the callback as given (dfinity/internet-identity#4305). It trusts a
local server by loopback host rather than by exact origin, stored
port-less as `http://127.0.0.1`, which is what lets this listener keep
binding port 0 for a fresh port per handshake. Standing in the check's
place are the user's deliberate opt-in to a local connector in II
Settings, II's notice before the first local sign-in for an identity on
a machine, and its consent screen on every connect.

So the route is dead weight, and its doc comment ("fail-closed, so
serving this document is mandatory") now describes something that never
happens. Remove it, leaving the handshake two routes: the pinned
callback page and the slim redeem. The hosted server keeps serving its
allow-list — II still enforces it for an https origin.

Also record why `state` is load-bearing here in a way it isn't for a
hosted server: a hosted callback sits on a domain a random page can't
reach, while this listener is reachable by any page in the browser and
any process on the machine, so an unguessable `state` (a v4 UUID) is
what stops a forged POST to `/redeem` from injecting an identity the
user never chose. II treats it as opaque and only echoes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake requested review from a team and a balanced review from Copilot August 31, 2026 13:45

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

Removes the local callback allow-list now that Internet Identity’s paired loopback support exempts local servers.

Changes:

  • Reduces the login listener to callback and redeem routes.
  • Updates unit and end-to-end coverage.
  • Revises deployment documentation for loopback trust.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Documents the hosted-only allow-list requirement.
docs/scoping-local-deployment.md Updates local listener design and verification notes.
docs/local-deployment.md Describes the two-route login flow.
crates/imcp2-local/src/login/tests.rs Verifies retired paths return 404.
crates/imcp2-local/src/login/routes.rs Removes the allow-list route and state.
crates/imcp2-local/src/login/mod.rs Simplifies router setup and updates security rationale.
crates/imcp2-local/src/e2e_local_login.rs Models port-less loopback trust without allow-list fetching.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +318 to +320
5. Serve exactly two loopback routes: `GET /callback` (the pinned fragment-reading page) and
`POST /redeem` (slim redeem → `redeem_registration_delegation`). No `#4091` allow-list —
II exempts a local server from it, because it cannot fetch one from a loopback origin.
Comment thread README.md
Comment on lines +727 to +730
The check applies to this hosted server only. II exempts a server on loopback,
because browsers block its https document from fetching a `http://127.0.0.1`
URL and the check would fail closed on every local sign-in — so `imcp2-local`
serves no such document.
Comment on lines +96 to +102
/// Unguessable on purpose (a v4 UUID), and load-bearing in a way a hosted
/// server's `state` is not: a hosted callback sits on a domain a random
/// page can't reach, while this listener is reachable by any page in the
/// browser and any process on the machine. `state` is what stops a forged
/// POST to `/redeem` from injecting an identity the user never chose. II
/// treats it as opaque and only echoes it, so keeping it unpredictable is
/// entirely this side's job.
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