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..b492d54d --- /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. 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"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; + +// 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; + const isLocal = + host === "localhost" || + host === "127.0.0.1" || + host.endsWith(".localhost"); + + if (isLocal) { + // icp-cli sets up a local alias: http://id.ai.localhost:8000 + return "http://id.ai.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 agent = await HttpAgent.create({ + identity, + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); + + return Actor.createActor(idlFactory, { agent, canisterId }); +} +``` + +:::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. +::: + +## 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 query ({ 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 + // Replace with your actual async canister call, e.g.: + // ic_cdk::call::<_, (String,)>(some_canister_id, "some_method", ()).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 +``` + +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: + +```bash +# Test as the default identity (authenticated) +icp canister call backend whoAmI + +# 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" +``` + +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 +``` + +## 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. + +:::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: + + ```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.json5` 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 + +- **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. +- **Passing principal as a string argument** — the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter. +- **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 + +- [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 + +{/* 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 */}