Skip to content

docs: update the Internet Identity guides for @icp-sdk/auth v9 - #395

Merged
sea-snake merged 8 commits into
mainfrom
docs/auth-v9
Sep 16, 2026
Merged

sea-snake merged 8 commits into
mainfrom
docs/auth-v9

Conversation

@sea-snake

@sea-snake sea-snake commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@icp-sdk/auth v9 shipped, and the Internet Identity guides describe v8. Some of it no longer compiles, one recommendation is now the opposite of what the library wants, and the guide had nothing on sharing a sign-in across sibling subdomains.

docs/guides/authentication/internet-identity.mdx

  • identityProvider is a pair. It is { authorizeUrl, canisterId } in v9, and a string throws a TypeError. getIdentityProviderUrl() becomes getIdentityProvider() and returns both halves; only the URL differs locally, since system canisters run at their mainnet IDs on the local network. Updated at all four call sites, including the derivationOrigin example, which named https://id.ai without the /authorize path.
  • One client per page, disposed. The guide said to create a single instance and warned that several cause "race conditions with session storage". In v9 the sign-in lives in storage rather than in the instance, so clients are cheap and read the same sign-in; what matters is calling dispose().
  • Sessions, not a single delegation. The "delegations expire, set maxTimeToLive to 8 hours" advice is gone. v9 bounds the session (maxTimeToIdle, maxTimeToLive, both optional) and replaces the short-lived delegation for you, so the sign-in example asks for no bounds at all.
  • New: a Render section. getStatus()'s four states and subscribe(), including that a sign-out in one tab reaches the others with no reload, and that getIdentity() throws SessionNotHeldError while a sign-in exists this origin holds nothing for.
  • New: organization SSO. ssoDomain and isValidSsoDomain alongside the existing openIdProvider section. The check throws when its signal aborts rather than returning false, so the example distinguishes "publishes nothing" from "no verdict".
  • New: sharing a sign-in across sibling subdomains. The three steps, building on the existing Alternative origins section rather than repeating it: one shared derivationOrigin authorized by that origin's alternative-origins document, since principals are per origin and a shared cookie changes nothing on its own; the same cookie domain through CookieStateStorage, which means trusting every origin under it; and a /reauth route answering signed-in-elsewhere with a second client (transport: "redirect" because the request runs on load with no user gesture, prompt: "none", and hint so the provider cannot answer for a different account). Every page runs the load check, not only protected ones, or a visitor already signed in on a sibling sees a signed-out header. Links the client's shared sessions guide for the rest.
  • requestAttributes nonce. The examples passed a promise; it has been a callback (() => Promise<Uint8Array>) since v8, so neither example would have run.
  • Install command pinned to @icp-sdk/auth@9 @icp-sdk/core@5, the pair this page documents, so it does not drift when the next major lands. On npm the core pin is redundant (v9's peer range holds it at ^5), but pnpm and yarn take latest for a bare request and reduce the conflict to a warning.
  • Two "Common mistakes" bullets rewritten (session bounds, disposing clients) and a link to the v9 upgrade guide added.

docs/guides/security/identity-and-access-management.mdx

The session-timeout recommendation linked to IdleManager, a class v9 removed. It now shows the bounds II actually enforces, with the OWASP 30-minute figure expressed as maxTimeToIdle, and states the provider's own defaults (seven days idle, thirty days total) and the ten-minute floor both bounds are clamped to — verified against dfinity/internet-identity@main (sessions.rs:30-32, storage/account.rs:62,70) rather than the client's JSDoc. How long a sign-in lasts is the provider's policy, narrowed by consent and by any organization cap, so leaving signIn() unbounded is presented as the right default and narrowing as an application's own choice.

docs/guides/canister-calls/calling-from-clients.md

One comment: getIdentity() returns an Identity, not a DelegationIdentity.

Verification

node scripts/validate.js --all passes (210 files) and npm run build completes (210 pages). The first validate run failed on six em-dashes I had written, which is how I learned this repo forbids them in prose; all six are rewritten, and a seventh inside a code comment was removed after review.

Left alone deliberately: docs/references/internet-identity-spec.md, which is synced from dfinity/internet-identity and would be overwritten.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake requested review from a team as code owners September 15, 2026 19:03
sea-snake and others added 2 commits September 15, 2026 21:11
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review: Internet Identity guides for @icp-sdk/auth v9

Must fix

  • Em-dash in a code comment (docs/guides/authentication/internet-identity.mdx:218): // function that fetches it, which the client calls when it needs the value —. checkEmdash in scripts/validate.js skips fenced code, so CI cannot see this one, but the ban is repo-wide. A colon or a comma reads the same here.
  • Internal link is missing its section anchor (internet-identity.mdx:53): ../../references/system-canisters.md should be ../../references/system-canisters.md#using-system-canisters-in-local-development. The sentence backing the claim is line 250 of that page, inside ## Using system canisters in local development, and the page already uses that slug internally (line 192). Without the anchor the reader lands at the top of a 20-heading catalogue of NNS, CMC and ledger canisters.

Suggestions

  • State the version requirement in "Install frontend packages": npm install @icp-sdk/auth @icp-sdk/core is unpinned, and the page now documents a v9-only API that throws a TypeError on v8. dfinity/icskills#384 raises its prerequisite to >= 9.0.0; one sentence here keeps the two aligned and gives the "Upgrading to v9" link a reason to be read before the code rather than at the foot of the page.
  • isValidSsoDomain throws when its signal aborts (internet-identity.mdx:165-172): the library is explicit that an abandoned check is not a verdict, so AbortSignal.timeout(5_000) raises on a slow lookup while the example calls it bare inside if (await ...). The page already carries a Common-mistakes bullet for this exact shape around signIn().
  • signed-in-elsewhere has no way out in the new render example: the branch routes to showResume(status.principal), but getIdentity() throws SessionNotHeldError in that state, and acquiring a credential needs prompt: 'none' with a hint. A sentence saying so would close the loop, or a pointer to the shared sessions guide that walks through it.
  • Give the security page the actual numbers (identity-and-access-management.mdx:96-98): the section argues for narrowing but never states the defaults (seven days idle, thirty days total), which is what makes the OWASP 30-minute figure land. Worth adding that max_idle has a 10-minute floor, so 30 minutes is comfortably inside what II accepts. Related: the PR description says the page "notes that the provider's defaults are too long for sensitive data", and as written it does not.

Verified

  • Every v9 API claim against dfinity/icp-js-auth at tag 9.0.0: identityProvider as { authorizeUrl, canisterId } with both halves required together and a string or URL throwing TypeError (src/client/auth-client.ts:395-413), the mainnet canister default rdmx6-jaaaa-aaaaa-aaadq-cai (:43), nonce: () => Promise<Uint8Array> (:1055), synchronous isAuthenticated() alongside async getIdentity(): Promise<Identity> (:562, :529), the four SessionStatus states and which of them carry a principal (:296-304), subscribe() and dispose() (:650, :665), openIdProvider and ssoDomain as mutually exclusive (:458), isValidSsoDomain(domain, signal) (src/client/sso.ts:93), and both session bounds optional (:248-259).
  • The seven-day and thirty-day figures against Internet Identity itself rather than the library's JSDoc: DEFAULT_SESSION_TTL_NS and MAX_SESSION_TTL_NS are both 30 * DAY_NS (src/internet_identity/src/sessions.rs:30), DEFAULT_SESSION_IDLE_NS is 7 * DAY_NS and MIN_SESSION_IDLE_NS is 10 * MINUTE_NS (src/internet_identity/src/storage/account.rs:62,70). The "capped by the organization" claim holds as well: session_max_age_seconds is declared per domain in the well-known config.
  • getIdentity() returns Identity and not DelegationIdentity, so the calling-from-clients.md:310 comment fix is right.
  • Coverage across docs/: nothing outside this PR still carries v8-era auth API (IdleManager, idleOptions, IdbStorage, keyType, AuthClient.create, a bare identityProvider string) or stale delegation-lifetime advice. verifiable-credentials.md:175 keeps new URL(...) correctly, since that is @dfinity/verifiable-credentials rather than this client.
  • Mechanical checks: the new link target exists, frontmatter is intact on both pages, no dfx, no mo:base, no -- in prose, the new snippet sits outside any TabItem, both pages still end with ## Next steps, and validate and build_and_deploy are green.

Comment thread docs/guides/authentication/internet-identity.mdx Outdated
Comment thread docs/guides/authentication/internet-identity.mdx Outdated
Comment thread docs/guides/authentication/internet-identity.mdx Outdated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake

Copy link
Copy Markdown
Contributor Author

Thanks — all six points addressed in 74633e8, plus the three suggestions from @MRmarioruci.

Must fix

  • Em-dash in a code comment. Fixed; that file now has zero em-dashes, fenced or not. One correction to the framing though: the rule is prose-scoped by design (checkEmdash does if (inFence) continue;), and docs/ already contains six em-dashes inside code fences, all pre-existing — implicit-parameters.md:268,321,332, mutable-arrays.md:244, enhanced-multi-migration.md:65. So mine was consistent with the repo rather than slipping past the validator. Changed anyway, since a comma reads the same.
  • Missing section anchor. Fixed. Worth noting checkInternalLinks splits on # and only resolves the file, so a wrong slug would not have been caught; the slug is the one the page itself uses at :192.

Suggestions

  • Version requirement. Taken further than suggested: the install command is now npm install @icp-sdk/auth@9 @icp-sdk/core@5, with a sentence naming the pair and linking the upgrade guide. Unpinned, the page ages into being wrong the moment v10 ships. On npm alone the pin of core is redundant — npm install --dry-run @icp-sdk/auth@9 @icp-sdk/core resolves core to 5.4.0, because v9 declares peerDependencies: { "@icp-sdk/core": "^5" } and npm's resolver honours it — but pnpm and yarn take latest (6.1.0) for a bare request and reduce the peer conflict to a warning, so both majors are pinned.
  • isValidSsoDomain throws on abort. Correct, and the source is explicit: signal.throwIfAborted() twice plus if (signal.aborted) throw error;, with false reserved for a genuinely unusable domain. The example is now a named function that catches, distinguishing "publishes nothing" from "no verdict".
  • signed-in-elsewhere has no way out. The branch now says what the state means and that getIdentity() throws SessionNotHeldError until a credential is acquired. I left out the pointer to the shared sessions guide deliberately: this page never sets up CookieStateStorage, so linking a setup it does not teach would be scope creep.
  • The actual numbers. Added, verified against dfinity/internet-identity@main rather than the client's JSDoc: DEFAULT_SESSION_TTL_NS = MAX_SESSION_TTL_NS = 30 * DAY_NS and MIN_SESSION_TTL_NS = 10 * MINUTE_NS (src/internet_identity/src/sessions.rs:30-32), DEFAULT_SESSION_IDLE_NS = 7 * DAY_NS and MIN_SESSION_IDLE_NS = 10 * MINUTE_NS (src/internet_identity/src/storage/account.rs:62,70). So the ten-minute floor applies to both bounds, not only to idle.

You were also right that the description no longer matched the page: the "defaults are too long for sensitive data" framing was cut on purpose, since how long a sign-in lasts is the provider's policy and the client names no bounds of its own. The description is corrected.

validate passes on all 210 files and npm run build completes.

marc0olo
marc0olo previously approved these changes Sep 16, 2026

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up review: Internet Identity guides for @icp-sdk/auth v9

Fixed

  • Em-dash at internet-identity.mdx:218 is now a comma. Re-scanned all three files: zero em-dashes (the one remaining in calling-from-clients.md is the pre-existing <!-- Upstream: --> line, which checkEmdash skips by design).
  • Section anchor added: ../../references/system-canisters.md#using-system-canisters-in-local-development, matching the heading and the slug that page already uses internally at line 192.
  • Version requirement stated, and the pin turns out to be necessary rather than cosmetic: npm install @icp-sdk/auth@9 @icp-sdk/core@6 fails outright with ERESOLVE, because v9 declares @icp-sdk/core: ^5 as its peer range. The line as written installs cleanly, and the page's own imports (safeGetCanisterEnv, AttributesIdentity) both exist in 5.4.0, so no example here depends on core 6.
  • isValidSsoDomain now separates "the domain publishes nothing" from "the check timed out", which matches the library's own framing that an abandoned check is not a verdict on the domain.
  • signed-in-elsewhere now names SessionNotHeldError and says getIdentity() throws until this origin holds a credential. Correct per auth-client.ts:545-557.
  • The security page carries the numbers now, and the clamping claim holds in both directions: MIN_SESSION_TTL_NS (sessions.rs:32) and MIN_SESSION_IDLE_NS (storage/account.rs:62) are both 10 * MINUTE_NS, so "both bounds are clamped with a ten-minute floor" is right, and the thirty minutes above sits inside it.

Not this PR's to fix, but now visible: @icp-sdk/core latest is 6.1.0, which is also this repo's watched pin, while guides/frontends/frameworks.md (twice), guides/canister-calls/calling-from-clients.md and guides/digital-assets/wallet-integration.md all carry a bare npm install @icp-sdk/core. A reader who follows one of those and then adds @icp-sdk/auth@9 hits the same ERESOLVE this page now avoids. Worth a separate issue: either pin core alongside auth on those pages, or wait for v9's peer range to catch up with core 6.

sea-snake and others added 2 commits September 16, 2026 17:45
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up review: Internet Identity guides for @icp-sdk/auth v9

Re-reviewing at 2f59b7f. Everything from the previous rounds still holds, and step 4 now covers the page that is already open, which matches what the skill PR does. The new "Sharing a sign-in across sibling subdomains" section needs another pass, though, and the first two points below apply to dfinity/icskills#384 as well.

Still needs work

  • transport: "redirect" has a prerequisite the section does not mention. Internet Identity delivers a redirect response only to a callback that the callback's own origin declares at /.well-known/ii-auth-callbacks, and this is enforced by the URL transport rather than only by the MCP flow: src/frontend/src/lib/utils/transport/url.ts:55 imports matchDeclaredCallback, and authCallbacks.ts states that the name "is deliberately not MCP-specific". The validation fails closed, so as written /reauth never comes back. What a reader needs: an entry that byte-exactly matches https://<origin>/reauth with no fragment, served as application/json with Access-Control-Allow-Origin (II reads it cross-origin), under the size cap, and without redirects, on each app origin rather than once on the derivation origin. This page already explains that _headers pattern twice, for ii-alternative-origins and ii-app-metadata, so a third block would fit the shape it already has. Worth carrying the upstream caveat too: a declared callback must terminate locally, because the response arrives in the URL fragment and a 3xx whose Location carries none re-attaches it to the redirect target.
  • The reason to pass hint is not what the section says. "Without hint the provider may answer for a different account, signing the user in as someone else" does not happen. Where Internet Identity holds more than one session and the request names none, it refuses: InteractionRequiredError.reason is account_selection_required (session-delegation.ts). Where a mint does answer for an account the client was not expecting, the client rejects it as AccountMismatchError, which is documented as "neither gone nor retryable" with "the caller's own record is what has to give way", so the session is dropped rather than the wrong account adopted. Passing hint is still right; the consequence of omitting it is a failed resume, not a wrong identity.
  • Step 2's returnTo contradicts the prose under it. The snippet hardcodes returnTo: "/", while the next paragraph says /reauth passes that next as returnTo. Reading new URLSearchParams(location.search).get("next") ?? "/" is what the prose describes.
  • The /reauth snippet is not self-contained. It reads status.principal, but status is created in step 3, which runs on the other pages rather than on this route, so the route never reads the status it takes the principal from. Constructing a client and reading getStatus() inside step 2 would close that.

Smaller, in the same section: CookieStateStorage is used without showing its import, unlike the page's other snippets, and clientOptions omits identityProvider while every other example on the page passes getIdentityProvider(), so in local development these two clients alone would point at mainnet Internet Identity.

I am dismissing my earlier approval, which was recorded against 74633e8 and predates this section.

@marc0olo
marc0olo dismissed their stale review September 16, 2026 16:15

This approval was recorded against 74633e8 and predates the shared-subdomain section added in 50af75b and 2f59b7f, which has open must-fix items. See the follow-up review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake

Copy link
Copy Markdown
Contributor Author

Both confirmed in source, and the callback one was a real hole: a generated app would have deployed and never returned from /reauth.

ii-auth-callbacks. authCallbacks.ts on II main spells out every constraint you listed: exact match against the declared entry, same-origin, no fragment, application/json under an 8 KiB cap, CORS so II can read it cross-origin, a fetch that refuses redirects and never caches, and "every failure fails the flow (closed)". AUTH_CALLBACKS_PATH is commented "Same origin only — different origins are separate trusted-server entries, each with its own file", which settles the per-origin question, and the file carries the terminate-locally caveat about a 3xx re-attaching the fragment. Both documents now say each app origin serves its own file listing its own /reauth, with the _headers block in the skill alongside the two it already had.

hint. You are right and the previous wording was wrong. InteractionRequiredError.reason is documented as account_selection_required "where it holds more than one and the request named none", and AccountMismatchError is "neither gone nor retryable" with "the caller's own record is what has to give way" — thrown at session-identity.ts:159 and caught at auth-client.ts:1515, which drops the session rather than adopting the account. Both places now say omitting hint costs the resume, not the user's identity.

Also fixed on the docs side, all four: the hardcoded returnTo: "/" now reads next, the /reauth snippet constructs its own client and reads getStatus() so it is self-contained, the CookieStateStorage import is shown, and clientOptions passes getIdentityProvider() like every other example on the page.

Eval results, which I owed you from the previous round as well:

Case With skill Baseline
22. Shared sign-in across sibling subdomains 9/9 1/9
23. Adversarial: shared cookie, different principals 4/4 3/4
24. Adversarial: silent re-issue without hint 4/4 2/4
25. Adversarial: redirecting an already-open page 3/3 2/3

Case 22 now asserts the callback document too, and your last point was exactly right: the case had been asking what the derivation origin serves, so it passed while the generated app could not have completed a sign-in. It now asks which origin serves each well-known document, and that expectation failed on the first run after I added it — the model produced ii-auth-callbacks and dropped ii-alternative-origins — which is what got the prompt corrected rather than the score accepted.

Case 24 gained a behavior asserting the failure is a refused resume rather than a wrong identity, so the corrected wording is now tested rather than just written.

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at 96d7b69, which is the head my dismissed approval did not cover.

All four points on the shared-subdomain section are fixed and checked against source: the /.well-known/ii-auth-callbacks document is documented per app origin with the exact-match, no-fragment, application/json and CORS rules and the fails-closed and terminate-locally caveats; the hint paragraph now names the account_selection_required refusal and the client-side AccountMismatchError, so it says what omitting it actually costs; returnTo reads the next parameter the prose describes; and the /reauth snippet constructs its own client and reads getStatus(), with the CookieStateStorage import shown and clientOptions passing getIdentityProvider() like the rest of the page.

Also still holding from the earlier rounds: no em-dashes, the section anchor on the system-canisters.md link, the @icp-sdk/auth@9 and @icp-sdk/core@5 pin that npm actually resolves, the guarded isValidSsoDomain call, and the session figures on the security page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake merged commit 64bb2c0 into main Sep 16, 2026
8 checks passed
@sea-snake
sea-snake deleted the docs/auth-v9 branch September 16, 2026 19:45
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