Skip to content

Fail a sign-in whose session vanished mid-call, instead of cutting off every code past the TTL - #236

Draft
aterga wants to merge 4 commits into
mainfrom
fix/redeem-no-recreated-session
Draft

aterga wants to merge 4 commits into
mainfrom
fix/redeem-no-recreated-session

Conversation

@aterga

@aterga aterga commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #234. #234 refused a code to any sign-in that finished after the 10-minute CONNECT_TTL. This PR replaces that blanket cutoff: a redemption now fails only when its session is actually gone.

The underlying bug (in imcp2-core):

  • redeem_registration_delegation called ensure_session before the mcp_register_v2 call, and set_grant_expiration/set_permissions after it.
  • If the session was reaped while that call was in flight (15 minutes after its connect began, or by eviction at capacity), it was silently re-created with a fresh key II never registered.
  • A code minted for that session led to a session that couldn't sign anything.

Why #234's cutoff was too broad: II keeps one MCP session per user, and a successful mcp_register_v2 replaces the previous one (see II's mcp_register_v2_is_multi_use_but_sessions_replace). So refusing the code for a registration that started in time and succeeded left a reconnecting user with neither their old session nor the new one.

What changes:

  • imcp2-core no longer re-creates the session on the redeem path. If the session is gone, or no longer holds the key II registered, the redemption fails cleanly with SIGN_IN_DROPPED_MSG and logs a warning.
  • The hosted mint step drops its TTL cutoff, so a redemption that started in time gets its code.
  • The TTL still bounds when a redemption may start, through the gate and the claim-time check under the lock.

Related issues

Follow-up to #234.

Changes

imcp2-core: identities.rs

  • redeem_registration_delegation looks the connect's session up instead of calling ensure_session. If the session is missing, it fails with the new pub const SIGN_IN_DROPPED_MSG before any network call. The message is written for users, because both callback pages show it verbatim.
  • New record_registration records the grant's expiration and access level in one step under the lock.
    • It only records if the session still exists and holds the exact key (session_der) that II just registered.
    • Otherwise it records nothing and fails with SIGN_IN_DROPPED_MSG.
    • In that case it also logs a warn! with the instance, the session id, and whether the session was gone or re-created with another key. It logs after releasing the lock. II has already replaced the user's previous session at this point, so operators need the trace (suggested by the Copilot review).
  • set_grant_expiration / set_permissions keep their behavior. Their docs now say redemption doesn't use them.
    • The shared logic moved into two free helpers: record_grant (the not-live → live transition and last_seen) and access_level (permissions parsing).
    • The "session opened" log is now log_session_opened.
  • PENDING_CONNECT_TTL_NS: the doc now says what the 15-minute reap can cut short, and that it fails cleanly.

src/auth.rs

  • reserve_code no longer refuses a connect past CONNECT_TTL. It refuses only when the connect itself is gone (evicted at the cap, or forgotten past EXPIRED_CONNECT_RETENTION), and in that case still discards the granted session.
  • claim_redemption keeps its TTL recheck. Its comment now explains that the TTL bounds when a redemption may start.

imcp2-local uses the same redeem path. Its connect creates the session first (registration_pubkey_b64), so the change is transparent there.

The branch merges main (7cdc218), which brings in #200 and #237. That merge kept main's discover.rs, so this PR's diff against main is limited to identities.rs and auth.rs.

Testing

Test Covers
record_registration_records_on_the_session_it_registered (new) Grant and access level are recorded together.
record_registration_refuses_a_session_gone_mid_call (new) A session reaped mid-call is refused and not re-created.
record_registration_refuses_a_session_re_created_with_another_key (new) Same id but another key: refused, no grant recorded. Fails without the key comparison.
redeem_refuses_a_connect_whose_session_is_gone (new) Fails up front without re-creating the session. Fails with the old ensure_session.
a_redemption_started_in_time_gets_its_code_even_past_the_ttl (replaces no_code_is_minted_for_a_connect_past_its_ttl) A redemption claimed in time gets its code past the TTL, and its session keeps the grant. Fails with #234's mint-time cutoff.
a_redemption_whose_connect_is_gone_gets_no_code (new) A vanished connect gets no code, and the granted session is discarded.
redeem_guards_missing_key_and_empty_chain (updated) Now creates the session without a registration key, since redeem no longer creates it.
  • cargo build --locked --all-targets
  • cargo test --locked --workspace --all-targets: all pass on the merge with main (imcp2 lib 91, bin 8, routers 11; imcp2-core 220; imcp2-local 21).
  • cargo fmt --all / cargo clippy --workspace --all-targets (tree is warning-free). --all-features, which includes the e2e code, also compiles. The End-to-end (PocketIC) CI job, which runs the real II handshake, passed on f8bcdd3.
  • npm test --prefix monitoring/mcp-status (n/a, dashboard untouched)

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VP2vTJm9ipTPD18xW9HyVG

…L cutoff

`redeem_registration_delegation` called `ensure_session` before the
`mcp_register_v2` call, and `set_grant_expiration`/`set_permissions` after
it, so a session reaped (15 minutes after its connect began) or evicted at
capacity while the call was in flight was silently re-created with a fresh
key that II never registered. A code minted for it led to a session that
could not sign anything. #234 avoided that by refusing every code past the
10-minute CONNECT_TTL, at the cost of throwing away registrations that
started in time and had succeeded.

`imcp2-core` now looks the session up instead of creating it, and records the
outcome through `record_registration`, which requires the session to still
hold the exact key II registered. Otherwise the redemption fails with
`SIGN_IN_DROPPED_MSG` and nothing is re-created.

With that, the hosted mint step no longer refuses a connect past its TTL: the
TTL bounds when a redemption may start (the gate, and the claim under the
lock), and one that started in time gets its code. That matters because II
keeps one MCP session per user; a successful registration has already
replaced the user's previous one, so refusing the code left them with
neither. `reserve_code` still refuses, and discards the granted session,
when the connect itself is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VP2vTJm9ipTPD18xW9HyVG

aterga commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

The red test job on efb203d didn't come from this PR. The only failure, with 219 tests passing, was discover::tests::resolve_app_identity_skips_probe_for_known_apps. It was a stale live-network expectation and failed the same way on main: oisy.com now publishes its own ii-derivation-origin, so it correctly resolves as Declared.

Update: main has since fixed that test in #237. This branch now merges main (7cdc218) and uses main's discover.rs, so the copy of #238's change it briefly carried is gone. #236's diff against main is back to identities.rs and auth.rs only. cargo test --locked --workspace --all-targets passes locally on the merge.


Generated by Claude Code

`resolve_app_identity_skips_probe_for_known_apps` resolved oisy.com and
expected `DerivationSource::Known`. oisy.com now publishes its own
/.well-known/ii-derivation-origin (`https://oisy.com`), and the app's own
declaration deliberately wins over the registry, so it resolves as
`Declared` and the test failed on main and on every PR. The test also pinned
the over-cap Layer 5 regression on oisy.com serving its SPA shell there,
which it no longer does.

- The live test now checks three registry apps that still declare nothing
  (nns.internetcomputer.org, multidex.ai, app.icpswap.com). Each must
  resolve to its registry derivation origin, unprobed, either via the
  registry (`Known`) or via its own agreeing declaration (`Declared`), and
  at least one must still go through the registry. One app adopting the
  declaration no longer breaks the test.
- The over-cap regression gets an offline test,
  `an_over_cap_layer5_body_reads_as_no_declaration`: a loopback listener
  serves an oversized page, and `fetch_well_known` must read it as `Absent`
  under `Overflow::NotTheDocument` and `Unreachable` under
  `Overflow::Unknown`.
- `Overflow::NotTheDocument`'s doc no longer says OISY serves that page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VP2vTJm9ipTPD18xW9HyVG

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.

🟡 Changes recommended

Missing or re-keyed sessions can cause unlogged redemption failures after registration succeeds and the prior MCP session has been replaced, leaving operators without a diagnostic.

1 open issue
What changed in this PR

This follow-up to #234 replaces the blanket sign-in completion cutoff with session/key validation, preventing unusable sessions without rejecting valid late completions.

Changes:

  • Reject missing or re-keyed sessions without recreating them, and record grants and permissions atomically.
  • Allow hosted redemptions started within the TTL to finish and receive authorization codes.
  • Add regression tests for session loss, key replacement, late completion, and vanished connects.
File Description
src/​auth.rs Removes the mint-time TTL cutoff and updates redemption tests and comments.
crates/​imcp2-core/​src/​identities.rs Validates session keys before recording registration, adds a user-facing failure message, and tests session-loss handling.

Review effort: Max

Comment thread crates/imcp2-core/src/identities.rs Resolved
main now carries #237's fix for the stale oisy.com discover test, so the
copy of #238's version this branch carried is dropped in favour of main's
discover.rs, and #200's auth.rs changes merge cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VP2vTJm9ipTPD18xW9HyVG
When `record_registration` refuses because the connect's session is gone, or
was re-created with another key, while `mcp_register_v2` ran, II has already
registered the key and replaced the user's previous MCP session. The user
only saw the callback page's message; nothing reached the logs. Warn with the
instance, session id, and which of the two happened, after the session lock
is released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VP2vTJm9ipTPD18xW9HyVG

This branch has not been deployed

No deployments
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