From 90f58017c0af0690143121c8e2430cb541a8df79 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 20 Mar 2026 09:17:05 +0100 Subject: [PATCH 1/5] docs: Internet Identity authentication guide --- .../authentication/internet-identity.md | 23 -- .../authentication/internet-identity.mdx | 328 ++++++++++++++++++ 2 files changed, 328 insertions(+), 23 deletions(-) delete mode 100644 docs/guides/authentication/internet-identity.md create mode 100644 docs/guides/authentication/internet-identity.mdx diff --git a/docs/guides/authentication/internet-identity.md b/docs/guides/authentication/internet-identity.md deleted file mode 100644 index 87143994..00000000 --- a/docs/guides/authentication/internet-identity.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: "Internet Identity" -description: "Integrate passkey-based authentication with Internet Identity" -sidebar: - order: 1 ---- - -TODO: Write content for this page. - - -Integrate Internet Identity (II) for passkey-based user authentication. Cover frontend setup with @icp-sdk/auth (AuthClient), delegation handling, principal-per-app isolation, and alternative origins configuration. Include Unity native app integration via deep links. Explain the delegation chain and session management. - - -- Portal: building-apps/authentication/overview.mdx, integrate-internet-identity.mdx, alternative-origins.mdx -- icskills: internet-identity -- JS SDK: @icp-sdk/auth (https://js.icp.build/auth) -- Examples: internet_identity_integration (Motoko), encrypted-notes-dapp-vetkd (both), native-apps/unity_ii_* (3 variants) - - -- concepts/security -- identity and trust -- guides/frontends/frameworks -- framework-specific auth setup -- guides/defi/wallet-integration -- alternative to II -- reference/internet-identity-spec -- protocol details diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx new file mode 100644 index 00000000..1b53daa2 --- /dev/null +++ b/docs/guides/authentication/internet-identity.mdx @@ -0,0 +1,328 @@ +--- +title: "Internet Identity" +description: "Integrate passkey-based authentication with Internet Identity for frontend login, backend caller verification, and session management" +sidebar: + order: 1 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Internet Identity (II) is the Internet Computer's native authentication system. Users sign in with passkeys or OpenID accounts (Google, Apple, Microsoft) instead of passwords. Each user receives a unique principal per frontend origin, preventing cross-app tracking. + +This guide covers setting up II authentication end-to-end: configuring your project, adding login to your frontend, and verifying callers in your backend. + +## How it works + +When a user authenticates through Internet Identity, the following happens: + +1. Your frontend opens an II popup window. +2. The user authenticates with a passkey or OpenID provider. +3. II creates a **delegation identity** — a temporary key pair that can sign messages on behalf of the user's master key. +4. Your frontend receives this delegation and uses it to sign canister calls. +5. The backend canister sees the user's **principal** (derived from the delegation chain) as `msg.caller`. + +**Principal-per-app isolation:** II derives a different principal for each frontend origin. A user logging into `https://app-a.icp0.io` gets a different principal than when logging into `https://app-b.icp0.io`, even with the same passkey. This prevents apps from correlating users across services. + +**Delegations expire.** The frontend sets a `maxTimeToLive` when requesting the delegation (default recommendation: 8 hours). After expiry, the user must re-authenticate. The maximum allowed delegation lifetime is 30 days (2,592,000,000,000,000 nanoseconds). + +## Project setup + +### Configure icp.yaml for local Internet Identity + +Add `ii: true` to your local network configuration. This tells icp-cli to deploy a local Internet Identity canister automatically: + +```yaml +networks: + - name: local + mode: managed + ii: true +``` + +### Install frontend packages + +```bash +npm install @icp-sdk/auth @icp-sdk/core +``` + +## Frontend integration + +The `AuthClient` from `@icp-sdk/auth` handles the full login flow: opening the II popup, receiving the delegation, and managing session persistence. + +### Environment detection + +Internet Identity runs at different URLs in local development versus mainnet. Detect the environment and resolve the correct URL at runtime: + +```javascript +import { AuthClient } from "@icp-sdk/auth/client"; +import { HttpAgent, Actor } from "@icp-sdk/core/agent"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; + +function getIdentityProviderUrl() { + const host = window.location.hostname; + const isLocal = + host === "localhost" || + host === "127.0.0.1" || + host.endsWith(".localhost"); + + if (isLocal) { + // icp-cli injects canister IDs via the ic_env cookie (set by the asset canister). + const canisterEnv = safeGetCanisterEnv(); + const iiCanisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"]; + if (!iiCanisterId) { + throw new Error("Could not find local II canister ID. Is Internet Identity deployed?"); + } + return `http://${iiCanisterId}.localhost:8000`; + } + return "https://id.ai"; +} +``` + +### Login, logout, and session check + +Create a single `AuthClient` instance on page load and reuse it for all operations: + +```javascript +// Create the auth client (once, on page load) +const authClient = await AuthClient.create(); + +// Check for existing session +const isAuthenticated = await authClient.isAuthenticated(); +if (isAuthenticated) { + const identity = authClient.getIdentity(); + // Restore session — create agent and actor with this identity +} + +// Login +async function login() { + return new Promise((resolve, reject) => { + authClient.login({ + identityProvider: getIdentityProviderUrl(), + maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours + onSuccess: () => { + const identity = authClient.getIdentity(); + console.log("Logged in as:", identity.getPrincipal().toText()); + resolve(identity); + }, + onError: (error) => { + console.error("Login failed:", error); + reject(error); + }, + }); + }); +} + +// Logout +async function logout() { + await authClient.logout(); + // Reset UI state or reload +} +``` + +### Create an authenticated agent + +After login, create an `HttpAgent` using the delegation identity. The agent signs all subsequent canister calls with the user's delegated key: + +```javascript +async function createAuthenticatedActor(identity, canisterId, idlFactory) { + const isLocal = + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" || + window.location.hostname.endsWith(".localhost"); + + const agent = await HttpAgent.create({ + identity, + host: isLocal ? "http://localhost:8000" : "https://icp-api.io", + ...(isLocal && { shouldFetchRootKey: true, verifyQuerySignatures: false }), + }); + + return Actor.createActor(idlFactory, { agent, canisterId }); +} +``` + +:::caution +Never set `shouldFetchRootKey: true` in production. Fetching the root key on mainnet is a security risk — it is only needed for local development where the replica's root key is not pre-trusted. +::: + +## Backend authentication + +Your backend canister receives the caller's principal automatically through the IC protocol. You do not pass the principal as a function argument — use `msg.caller` (Motoko) or `ic_cdk::api::msg_caller()` (Rust) to read it. + +### Reject anonymous callers + +Any unauthenticated request uses the anonymous principal (`2vxsx-fae`). Reject it in protected endpoints: + + + + +```motoko +import Principal "mo:core/Principal"; +import Runtime "mo:core/Runtime"; + +persistent actor { + func requireAuth(caller : Principal) : () { + if (Principal.isAnonymous(caller)) { + Runtime.trap("Anonymous principal not allowed."); + }; + }; + + public shared ({ caller }) func whoAmI() : async Text { + if (Principal.isAnonymous(caller)) { + "anonymous" + } else { + Principal.toText(caller) + }; + }; + + public shared ({ caller }) func protectedAction() : async Text { + requireAuth(caller); + "Action performed by " # Principal.toText(caller) + }; +}; +``` + + + + +```rust +use candid::Principal; +use ic_cdk::{query, update}; + +fn require_auth() -> Principal { + let caller = ic_cdk::api::msg_caller(); + if caller == Principal::anonymous() { + ic_cdk::trap("Anonymous principal not allowed."); + } + caller +} + +#[query] +fn who_am_i() -> String { + let caller = ic_cdk::api::msg_caller(); + if caller == Principal::anonymous() { + "anonymous".to_string() + } else { + format!("{}", caller) + } +} + +#[update] +fn protected_action() -> String { + let caller = require_auth(); + format!("Action performed by {}", caller) +} +``` + + + + +### Rust: capture caller before await + +In async update functions, bind the caller at the top of the function before any `.await` points. The current ic-cdk executor preserves the caller across await points, but capturing it early is a defensive practice that guards against future executor changes: + +```rust +#[update] +async fn protected_async_action() -> String { + let caller = require_auth(); // Capture before any await + let _result = some_async_operation().await; + format!("Action completed by {}", caller) +} +``` + +## Local development + +Start the local network and deploy. With `ii: true` in your `icp.yaml`, icp-cli deploys a local Internet Identity canister automatically: + +```bash +icp network start +icp deploy +``` + +The local II canister gets a dynamically assigned canister ID. The asset canister makes this ID available to your frontend via the `ic_env` cookie, which you can read using `safeGetCanisterEnv` from `@icp-sdk/core/agent/canister-env` (shown in the environment detection section above). + +To test authentication from the command line: + +```bash +# Test as the default identity (authenticated) +icp canister call backend whoAmI + +# Test as anonymous +icp identity use anonymous +icp canister call backend protectedAction +# Expected: Error containing "Anonymous principal not allowed" +icp identity use default +``` + +For mainnet deployment, Internet Identity is already running at canister ID `rdmx6-jaaaa-aaaaa-aaadq-cai` (`https://id.ai`). Deploy only your own canisters: + +```bash +icp deploy -e ic +``` + +## Alternative origins + +By default, each frontend origin produces a different user principal. If you serve your app from multiple domains (for example, migrating from `.icp0.io` to a custom domain), users would get different principals on each domain. + +To keep principals consistent across domains, configure **alternative origins**: + +1. **On the primary origin (A):** Create a file at `.well-known/ii-alternative-origins` listing the alternative domains: + + ```json + { + "alternativeOrigins": ["https://www.yourcustomdomain.com"] + } + ``` + + A maximum of 10 alternative origins can be listed. No trailing slashes or paths. + +2. **Configure the asset canister** to serve the `.well-known` directory. Add an `.ic-assets.json` in your frontend source: + + ```json + [ + { + "match": ".well-known", + "ignore": false + }, + { + "match": ".well-known/ii-alternative-origins", + "headers": { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json" + }, + "ignore": false + } + ] + ``` + +3. **On the alternative origin (B):** Set the `derivationOrigin` in your login call to point back to the primary origin: + + ```javascript + authClient.login({ + identityProvider: "https://id.ai", + derivationOrigin: "https://xxxxx.icp0.io", // primary origin A + onSuccess: () => { /* ... */ }, + }); + ``` + + The primary origin (A) does not need `derivationOrigin` — it is only required on alternative origins. + +For full details, see the [Internet Identity specification](../../reference/internet-identity-spec.md). + +## Common mistakes + +- **Hardcoding the II URL** — use environment detection (shown above) to switch between `localhost` and `https://id.ai`. Hardcoding one breaks the other environment. +- **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed. +- **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps. +- **Passing principal as a string argument** — the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter. +- **Skipping `fetchRootKey` locally** — without `shouldFetchRootKey: true` on local agents, certificate verification fails. +- **Creating multiple `AuthClient` instances** — create one on page load and reuse it. Multiple instances cause race conditions with session storage. + +## Next steps + +- [Wallet integration](../defi/wallet-integration.md) for token-based authentication alternatives +- [Frontend frameworks](../frontends/frameworks.md) for framework-specific auth setup patterns +- [Internet Identity specification](../../reference/internet-identity-spec.md) for protocol details and the full alternative origins spec +- [Security best practices](../../concepts/security.md) for identity and trust fundamentals +- [AuthClient API reference](https://js.icp.build) for the full `@icp-sdk/auth` API + +{/* Upstream: informed by dfinity/portal — docs/building-apps/authentication/overview.mdx, docs/building-apps/authentication/integrate-internet-identity.mdx, docs/building-apps/authentication/alternative-origins.mdx; dfinity/icskills — skills/internet-identity/SKILL.md */} From 31a8474414cf8ab0c67897a9ff1f4b38fb477b41 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 20 Mar 2026 11:49:05 +0100 Subject: [PATCH 2/5] fix: hardcode well-known II canister ID, add fetch binding per icskills#91 --- .../authentication/internet-identity.mdx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx index 1b53daa2..415041cf 100644 --- a/docs/guides/authentication/internet-identity.mdx +++ b/docs/guides/authentication/internet-identity.mdx @@ -50,12 +50,15 @@ The `AuthClient` from `@icp-sdk/auth` handles the full login flow: opening the I ### Environment detection -Internet Identity runs at different URLs in local development versus mainnet. Detect the environment and resolve the correct URL at runtime: +Internet Identity runs at different URLs in local development versus mainnet. Since II has a well-known canister ID (`rdmx6-jaaaa-aaaaa-aaadq-cai`) that is the same on mainnet and local replicas, you only need to detect the host: ```javascript import { AuthClient } from "@icp-sdk/auth/client"; import { HttpAgent, Actor } from "@icp-sdk/core/agent"; -import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; + +// Internet Identity has a well-known canister ID that is the same on mainnet +// and local replicas (icp-cli pulls the mainnet II wasm). +const II_CANISTER_ID = "rdmx6-jaaaa-aaaaa-aaadq-cai"; function getIdentityProviderUrl() { const host = window.location.hostname; @@ -65,14 +68,7 @@ function getIdentityProviderUrl() { host.endsWith(".localhost"); if (isLocal) { - // icp-cli injects canister IDs via the ic_env cookie (set by the asset canister). - const canisterEnv = safeGetCanisterEnv(); - const iiCanisterId = - canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"]; - if (!iiCanisterId) { - throw new Error("Could not find local II canister ID. Is Internet Identity deployed?"); - } - return `http://${iiCanisterId}.localhost:8000`; + return `http://${II_CANISTER_ID}.localhost:8000`; } return "https://id.ai"; } @@ -133,6 +129,7 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) { const agent = await HttpAgent.create({ identity, host: isLocal ? "http://localhost:8000" : "https://icp-api.io", + fetch: window.fetch.bind(window), // prevents "Illegal invocation" in bundled builds ...(isLocal && { shouldFetchRootKey: true, verifyQuerySignatures: false }), }); @@ -238,7 +235,7 @@ icp network start icp deploy ``` -The local II canister gets a dynamically assigned canister ID. The asset canister makes this ID available to your frontend via the `ic_env` cookie, which you can read using `safeGetCanisterEnv` from `@icp-sdk/core/agent/canister-env` (shown in the environment detection section above). +Internet Identity has a well-known canister ID (`rdmx6-jaaaa-aaaaa-aaadq-cai`) that is the same on mainnet and local replicas — icp-cli pulls the mainnet II Wasm when deploying locally. You can hardcode this ID in your frontend (shown in the environment detection section above). To test authentication from the command line: @@ -310,7 +307,8 @@ For full details, see the [Internet Identity specification](../../reference/inte ## Common mistakes -- **Hardcoding the II URL** — use environment detection (shown above) to switch between `localhost` and `https://id.ai`. Hardcoding one breaks the other environment. +- **Using the wrong II URL per environment** — local development must point to `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000`, mainnet to `https://id.ai`. Use environment detection (shown above) to switch automatically. +- **`fetch` "Illegal invocation" in bundled builds** — always pass `fetch: window.fetch.bind(window)` to `HttpAgent.create()`. Without explicit binding, bundlers (Vite, webpack) extract `fetch` from `window` and call it without the correct `this` context. - **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed. - **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps. - **Passing principal as a string argument** — the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter. From 2e08dce96b1fa57c0843c7beb6cd670a33b22e55 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 20 Mar 2026 11:51:42 +0100 Subject: [PATCH 3/5] fix: clarify II URL pitfall wording --- docs/guides/authentication/internet-identity.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx index 415041cf..9ed0fbd1 100644 --- a/docs/guides/authentication/internet-identity.mdx +++ b/docs/guides/authentication/internet-identity.mdx @@ -307,7 +307,7 @@ For full details, see the [Internet Identity specification](../../reference/inte ## Common mistakes -- **Using the wrong II URL per environment** — local development must point to `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000`, mainnet to `https://id.ai`. Use environment detection (shown above) to switch automatically. +- **Using the wrong II URL per environment** — local development must point to `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000`, mainnet to `https://id.ai`. Use the `getIdentityProviderUrl` helper (shown above) to switch based on hostname. - **`fetch` "Illegal invocation" in bundled builds** — always pass `fetch: window.fetch.bind(window)` to `HttpAgent.create()`. Without explicit binding, bundlers (Vite, webpack) extract `fetch` from `window` and call it without the correct `this` context. - **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed. - **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps. From 740b744efc03d02eb1a0e6b0bc684a9e710e091f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 15 Apr 2026 13:45:19 +0200 Subject: [PATCH 4/5] fix: address PR feedback on Internet Identity auth guide --- .../authentication/internet-identity.mdx | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx index 9ed0fbd1..d33f0c5f 100644 --- a/docs/guides/authentication/internet-identity.mdx +++ b/docs/guides/authentication/internet-identity.mdx @@ -50,15 +50,15 @@ The `AuthClient` from `@icp-sdk/auth` handles the full login flow: opening the I ### Environment detection -Internet Identity runs at different URLs in local development versus mainnet. Since II has a well-known canister ID (`rdmx6-jaaaa-aaaaa-aaadq-cai`) that is the same on mainnet and local replicas, you only need to detect the host: +Internet Identity runs at different URLs in local development versus mainnet. II uses a well-known frontend canister (`uqzsh-gqaaa-aaaaq-qaada-cai`) that you authenticate against. Detect the host to return the right URL: ```javascript import { AuthClient } from "@icp-sdk/auth/client"; import { HttpAgent, Actor } from "@icp-sdk/core/agent"; -// Internet Identity has a well-known canister ID that is the same on mainnet -// and local replicas (icp-cli pulls the mainnet II wasm). -const II_CANISTER_ID = "rdmx6-jaaaa-aaaaa-aaadq-cai"; +// Internet Identity authenticates users through its frontend canister. +// The frontend canister ID is well-known and the same on mainnet and local replicas. +const II_FRONTEND_CANISTER_ID = "uqzsh-gqaaa-aaaaq-qaada-cai"; function getIdentityProviderUrl() { const host = window.location.hostname; @@ -68,7 +68,8 @@ function getIdentityProviderUrl() { host.endsWith(".localhost"); if (isLocal) { - return `http://${II_CANISTER_ID}.localhost:8000`; + // icp-cli sets up a local alias: http://id.ai.localhost:8000 + return "http://id.ai.localhost:8000"; } return "https://id.ai"; } @@ -129,7 +130,7 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) { const agent = await HttpAgent.create({ identity, host: isLocal ? "http://localhost:8000" : "https://icp-api.io", - fetch: window.fetch.bind(window), // prevents "Illegal invocation" in bundled builds + fetch: window.fetch.bind(window), ...(isLocal && { shouldFetchRootKey: true, verifyQuerySignatures: false }), }); @@ -141,6 +142,8 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) { Never set `shouldFetchRootKey: true` in production. Fetching the root key on mainnet is a security risk — it is only needed for local development where the replica's root key is not pre-trusted. ::: +> **Note:** The current recommended approach is to pass the root key via the `IC_ROOT_KEY` environment variable rather than using `shouldFetchRootKey: true`. icp-cli environments can inject this automatically, avoiding the need to fetch it at runtime. See the [icp-cli documentation](https://cli.internetcomputer.org/) for how to configure this with icp-cli environments. + ## Backend authentication Your backend canister receives the caller's principal automatically through the IC protocol. You do not pass the principal as a function argument — use `msg.caller` (Motoko) or `ic_cdk::api::msg_caller()` (Rust) to read it. @@ -163,7 +166,7 @@ persistent actor { }; }; - public shared ({ caller }) func whoAmI() : async Text { + public shared query ({ caller }) func whoAmI() : async Text { if (Principal.isAnonymous(caller)) { "anonymous" } else { @@ -221,7 +224,8 @@ In async update functions, bind the caller at the top of the function before any #[update] async fn protected_async_action() -> String { let caller = require_auth(); // Capture before any await - let _result = some_async_operation().await; + // Replace with your actual async canister call, e.g.: + // ic_cdk::call::<_, (String,)>(some_canister_id, "some_method", ()).await format!("Action completed by {}", caller) } ``` @@ -235,7 +239,7 @@ icp network start icp deploy ``` -Internet Identity has a well-known canister ID (`rdmx6-jaaaa-aaaaa-aaadq-cai`) that is the same on mainnet and local replicas — icp-cli pulls the mainnet II Wasm when deploying locally. You can hardcode this ID in your frontend (shown in the environment detection section above). +icp-cli pulls the mainnet II Wasm when deploying locally and registers a local alias so the II frontend is reachable at `http://id.ai.localhost:8000`. Use the `getIdentityProviderUrl` helper (shown in the environment detection section above) to point to this URL in local development. To test authentication from the command line: @@ -244,10 +248,10 @@ To test authentication from the command line: icp canister call backend whoAmI # Test as anonymous -icp identity use anonymous +icp identity default anonymous icp canister call backend protectedAction # Expected: Error containing "Anonymous principal not allowed" -icp identity use default +icp identity default default ``` For mainnet deployment, Internet Identity is already running at canister ID `rdmx6-jaaaa-aaaaa-aaadq-cai` (`https://id.ai`). Deploy only your own canisters: @@ -260,7 +264,11 @@ icp deploy -e ic By default, each frontend origin produces a different user principal. If you serve your app from multiple domains (for example, migrating from `.icp0.io` to a custom domain), users would get different principals on each domain. -To keep principals consistent across domains, configure **alternative origins**: +:::note +II now automatically handles the `icp0.io` vs `ic0.app` domain difference — you do **not** need to use `derivationOrigin` or `ii-alternative-origins` for that case. Use alternative origins only when you have two genuinely distinct custom domains that should share the same user principal. +::: + +To keep principals consistent across your own custom domains, configure **alternative origins**: 1. **On the primary origin (A):** Create a file at `.well-known/ii-alternative-origins` listing the alternative domains: @@ -307,7 +315,7 @@ For full details, see the [Internet Identity specification](../../reference/inte ## Common mistakes -- **Using the wrong II URL per environment** — local development must point to `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000`, mainnet to `https://id.ai`. Use the `getIdentityProviderUrl` helper (shown above) to switch based on hostname. +- **Using the wrong II URL per environment** — local development must point to `http://id.ai.localhost:8000`, mainnet to `https://id.ai`. Use the `getIdentityProviderUrl` helper (shown above) to switch based on hostname. - **`fetch` "Illegal invocation" in bundled builds** — always pass `fetch: window.fetch.bind(window)` to `HttpAgent.create()`. Without explicit binding, bundlers (Vite, webpack) extract `fetch` from `window` and call it without the correct `this` context. - **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed. - **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps. @@ -323,4 +331,6 @@ For full details, see the [Internet Identity specification](../../reference/inte - [Security best practices](../../concepts/security.md) for identity and trust fundamentals - [AuthClient API reference](https://js.icp.build) for the full `@icp-sdk/auth` API +{/* TODO: Add Unity native app integration via deep links — see portal native-apps/unity_ii_* */} + {/* Upstream: informed by dfinity/portal — docs/building-apps/authentication/overview.mdx, docs/building-apps/authentication/integrate-internet-identity.mdx, docs/building-apps/authentication/alternative-origins.mdx; dfinity/icskills — skills/internet-identity/SKILL.md */} From 80674b4148aac211ff47284051ca41406aaeb9f8 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 15 Apr 2026 14:26:38 +0200 Subject: [PATCH 5/5] fix(internet-identity): use ic_env cookie for root key, clarify Node.js pattern - Replace shouldFetchRootKey/verifyQuerySignatures with safeGetCanisterEnv() pattern; rootKey: canisterEnv?.IC_ROOT_KEY works in both local and production without environment branching - Remove dead II_FRONTEND_CANISTER_ID constant - Add Node.js note: fetchRootKey() is acceptable for local-only scripts, never mainnet - Fix CLI test: use --identity flag instead of mutating global default identity - Mention both II canister IDs (backend + frontend); note they are identical locally with ii: true - Fix .ic-assets.json -> .ic-assets.json5 in alternative origins section - Update Common mistakes entry with correct nuanced guidance --- .../authentication/internet-identity.mdx | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx index d33f0c5f..b492d54d 100644 --- a/docs/guides/authentication/internet-identity.mdx +++ b/docs/guides/authentication/internet-identity.mdx @@ -55,10 +55,12 @@ Internet Identity runs at different URLs in local development versus mainnet. II ```javascript import { AuthClient } from "@icp-sdk/auth/client"; import { HttpAgent, Actor } from "@icp-sdk/core/agent"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -// Internet Identity authenticates users through its frontend canister. -// The frontend canister ID is well-known and the same on mainnet and local replicas. -const II_FRONTEND_CANISTER_ID = "uqzsh-gqaaa-aaaaq-qaada-cai"; +// Read the ic_env cookie set by the asset canister or Vite dev server. +// Contains IC_ROOT_KEY and canister IDs — works in both local and production without +// environment branching. Available in browser contexts only; see note below for Node.js. +const canisterEnv = safeGetCanisterEnv(); function getIdentityProviderUrl() { const host = window.location.hostname; @@ -122,28 +124,20 @@ After login, create an `HttpAgent` using the delegation identity. The agent sign ```javascript async function createAuthenticatedActor(identity, canisterId, idlFactory) { - const isLocal = - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1" || - window.location.hostname.endsWith(".localhost"); - const agent = await HttpAgent.create({ identity, - host: isLocal ? "http://localhost:8000" : "https://icp-api.io", - fetch: window.fetch.bind(window), - ...(isLocal && { shouldFetchRootKey: true, verifyQuerySignatures: false }), + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, }); return Actor.createActor(idlFactory, { agent, canisterId }); } ``` -:::caution -Never set `shouldFetchRootKey: true` in production. Fetching the root key on mainnet is a security risk — it is only needed for local development where the replica's root key is not pre-trusted. +:::note[Node.js environments] +`safeGetCanisterEnv()` reads the `ic_env` cookie set by the asset canister or Vite dev server — it only works in browser contexts. For Node.js scripts or tests connecting to a **local** replica, create the agent normally and call `await agent.fetchRootKey()` explicitly after creation. Never call `fetchRootKey()` against a mainnet endpoint — on mainnet the root key is pre-trusted, and fetching it at runtime exposes a man-in-the-middle risk. ::: -> **Note:** The current recommended approach is to pass the root key via the `IC_ROOT_KEY` environment variable rather than using `shouldFetchRootKey: true`. icp-cli environments can inject this automatically, avoiding the need to fetch it at runtime. See the [icp-cli documentation](https://cli.internetcomputer.org/) for how to configure this with icp-cli environments. - ## Backend authentication Your backend canister receives the caller's principal automatically through the IC protocol. You do not pass the principal as a function argument — use `msg.caller` (Motoko) or `ic_cdk::api::msg_caller()` (Rust) to read it. @@ -247,14 +241,12 @@ To test authentication from the command line: # Test as the default identity (authenticated) icp canister call backend whoAmI -# Test as anonymous -icp identity default anonymous -icp canister call backend protectedAction +# Test as anonymous using --identity to avoid changing your global default +icp canister call backend protectedAction --identity anonymous # Expected: Error containing "Anonymous principal not allowed" -icp identity default default ``` -For mainnet deployment, Internet Identity is already running at canister ID `rdmx6-jaaaa-aaaaa-aaadq-cai` (`https://id.ai`). Deploy only your own canisters: +For mainnet deployment, Internet Identity is already running — backend canister `rdmx6-jaaaa-aaaaa-aaadq-cai` and frontend canister `uqzsh-gqaaa-aaaaq-qaada-cai` (served at `https://id.ai`). Both IDs are identical on local replicas when `ii: true` is configured. Deploy only your own canisters: ```bash icp deploy -e ic @@ -280,7 +272,7 @@ To keep principals consistent across your own custom domains, configure **altern A maximum of 10 alternative origins can be listed. No trailing slashes or paths. -2. **Configure the asset canister** to serve the `.well-known` directory. Add an `.ic-assets.json` in your frontend source: +2. **Configure the asset canister** to serve the `.well-known` directory. Add an `.ic-assets.json5` in your frontend source: ```json [ @@ -320,7 +312,7 @@ For full details, see the [Internet Identity specification](../../reference/inte - **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed. - **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps. - **Passing principal as a string argument** — the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter. -- **Skipping `fetchRootKey` locally** — without `shouldFetchRootKey: true` on local agents, certificate verification fails. +- **Using `shouldFetchRootKey: true` in browser code** — pass `rootKey: canisterEnv?.IC_ROOT_KEY` from `safeGetCanisterEnv()` instead. `shouldFetchRootKey: true` fetches the root key from the replica at runtime, which lets a man-in-the-middle substitute a fake key on mainnet. For Node.js scripts targeting a local replica only, `await agent.fetchRootKey()` is acceptable — but never on mainnet. - **Creating multiple `AuthClient` instances** — create one on page load and reuse it. Multiple instances cause race conditions with session storage. ## Next steps