Skip to content
2 changes: 1 addition & 1 deletion .sources/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ ic-pub-key v1.0.1
icp-cli v0.2.3 caeac37
motoko v1.6.0 b818df6
motoko-core v2.4.0 cd37dbf
cdk-rs ic-cdk v0.19.0 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 80d8234
cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c
candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf
response-verification v3.1.0 18c5a37
2 changes: 1 addition & 1 deletion .sources/cdk-rs
Submodule cdk-rs updated 89 files
+82 −101 .github/workflows/ci.yml
+120 −0 .github/workflows/publish.yml
+49 −19 Cargo.toml
+4 −5 README.md
+25 −20 e2e-tests/Cargo.toml
+5 −8 e2e-tests/build.rs
+19 −0 e2e-tests/ic.did
+51 −34 e2e-tests/src/bin/api.rs
+1 −1 e2e-tests/src/bin/async.rs
+1 −1 e2e-tests/src/bin/bindgen_callee/main.rs
+23 −7 e2e-tests/src/bin/bitcoin_canister.rs
+1 −1 e2e-tests/src/bin/call.rs
+5 −3 e2e-tests/src/bin/canister_info.rs
+5 −5 e2e-tests/src/bin/chunk.rs
+4 −4 e2e-tests/src/bin/http_request.rs
+1 −1 e2e-tests/src/bin/macros/main.rs
+21 −1 e2e-tests/src/bin/management_canister.rs
+5 −10 e2e-tests/src/bin/timers.rs
+41 −4 e2e-tests/tests/api.rs
+18 −13 e2e-tests/tests/async.rs
+2 −2 e2e-tests/tests/bindgen.rs
+56 −94 e2e-tests/tests/bitcoin_canister.rs
+1 −1 e2e-tests/tests/canister_info.rs
+1 −1 e2e-tests/tests/http_request.rs
+5 −4 e2e-tests/tests/macros.rs
+4 −2 e2e-tests/tests/management_canister.rs
+21 −12 e2e-tests/tests/test_utilities.rs
+12 −6 e2e-tests/tests/timers.rs
+2 −0 ic-cdk-bindgen/CHANGELOG.md
+2 −2 ic-cdk-bindgen/Cargo.toml
+3 −3 ic-cdk-bindgen/README.md
+88 −0 ic-cdk-bitcoin-canister/CHANGELOG.md
+23 −0 ic-cdk-bitcoin-canister/Cargo.toml
+0 −0 ic-cdk-bitcoin-canister/LICENSE
+80 −0 ic-cdk-bitcoin-canister/README.md
+220 −0 ic-cdk-bitcoin-canister/src/lib.rs
+4 −4 ic-cdk-executor/Cargo.toml
+10 −5 ic-cdk-macros/Cargo.toml
+33 −0 ic-cdk-management-canister/CHANGELOG.md
+35 −0 ic-cdk-management-canister/Cargo.toml
+1 −0 ic-cdk-management-canister/LICENSE
+74 −0 ic-cdk-management-canister/README.md
+10 −64 ic-cdk-management-canister/src/lib.rs
+11 −7 ic-cdk-timers/Cargo.toml
+26 −29 ic-cdk-timers/src/global_timer.rs
+2 −3 ic-cdk-timers/src/state.rs
+14 −0 ic-cdk/CHANGELOG.md
+19 −22 ic-cdk/Cargo.toml
+4 −4 ic-cdk/README.md
+49 −142 ic-cdk/src/api.rs
+0 −1,008 ic-cdk/src/api/call.rs
+0 −142 ic-cdk/src/api/management_canister/bitcoin/mod.rs
+0 −196 ic-cdk/src/api/management_canister/bitcoin/types.rs
+0 −35 ic-cdk/src/api/management_canister/ecdsa/mod.rs
+0 −83 ic-cdk/src/api/management_canister/ecdsa/types.rs
+0 −108 ic-cdk/src/api/management_canister/http_request/mod.rs
+0 −136 ic-cdk/src/api/management_canister/http_request/types.rs
+0 −248 ic-cdk/src/api/management_canister/main/mod.rs
+0 −648 ic-cdk/src/api/management_canister/main/types.rs
+0 −42 ic-cdk/src/api/management_canister/mod.rs
+0 −59 ic-cdk/src/api/management_canister/provisional.rs
+0 −43 ic-cdk/src/api/management_canister/schnorr/mod.rs
+0 −86 ic-cdk/src/api/management_canister/schnorr/types.rs
+0 −28 ic-cdk/src/api/stable/canister.rs
+0 −465 ic-cdk/src/api/stable/mod.rs
+0 −449 ic-cdk/src/bitcoin_canister.rs
+10 −10 ic-cdk/src/call.rs
+0 −1 ic-cdk/src/futures.rs
+0 −22 ic-cdk/src/lib.rs
+2 −2 ic-cdk/src/macros.rs
+0 −148 ic-cdk/tests/bitcoin.did
+0 −59 ic-cdk/tests/bitcoin_candid_equality.rs
+0 −106 ic-management-canister-types/CHANGELOG.md
+0 −23 ic-management-canister-types/Cargo.toml
+0 −21 ic-management-canister-types/README.md
+0 −1,631 ic-management-canister-types/src/lib.rs
+0 −215 ic-management-canister-types/tests/candid_equality.rs
+6 −0 ic0/CHANGELOG.md
+2 −2 ic0/Cargo.toml
+4 −0 ic0/ic0.txt
+8 −0 ic0/manual_safety_comments.txt
+46 −0 ic0/src/lib.rs
+24 −0 ic0/src/sys.rs
+3 −3 library/ic-certified-map/Cargo.toml
+2 −2 library/ic-certified-map/src/lib.rs
+4 −5 library/ic-certified-map/src/rbtree.rs
+3 −4 library/ic-certified-map/src/rbtree/test.rs
+2 −2 library/ic-ledger-types/Cargo.toml
+39 −8 scripts/download_pocket_ic_server.sh
291 changes: 258 additions & 33 deletions docs/guides/authentication/internet-identity.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Internet Identity"
description: "Integrate passkey-based authentication with Internet Identity for frontend login, backend caller verification, and session management"
description: "Integrate passkey-based authentication with Internet Identity for frontend sign-in, backend caller verification, and session management"
sidebar:
order: 1
---
Expand All @@ -9,7 +9,7 @@ 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.
This guide covers setting up II authentication end-to-end: configuring your project, adding sign-in to your frontend, and verifying callers in your backend.

## How it works

Expand Down Expand Up @@ -46,7 +46,7 @@ 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.
The `AuthClient` from `@icp-sdk/auth` handles the full sign-in flow: opening the II popup, receiving the delegation, and managing session persistence.

### Environment detection

Expand Down Expand Up @@ -77,50 +77,61 @@ function getIdentityProviderUrl() {
}
```

### Login, logout, and session check
### Sign in, sign out, and session check

Create a single `AuthClient` instance on page load and reuse it for all operations:
Create a single `AuthClient` instance on page load and reuse it for all operations. The identity provider URL is passed at construction time, not on each sign-in:

```javascript
// Create the auth client (once, on page load)
const authClient = await AuthClient.create();
const authClient = new AuthClient({
identityProvider: getIdentityProviderUrl(),
});

// Check for existing session
const isAuthenticated = await authClient.isAuthenticated();
if (isAuthenticated) {
const identity = authClient.getIdentity();
if (authClient.isAuthenticated()) {
const identity = await authClient.getIdentity();
// Restore session: create agent and actor with this identity
}

// Login
async function login() {
return new Promise((resolve, reject) => {
authClient.login({
identityProvider: getIdentityProviderUrl(),
// Sign in
async function signIn() {
try {
const identity = await authClient.signIn({
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);
},
});
});
console.log("Signed in as:", identity.getPrincipal().toText());
return identity;
} catch (error) {
console.error("Sign-in failed:", error);
throw error;
}
}

// Logout
async function logout() {
await authClient.logout();
// Sign out
async function signOut() {
await authClient.signOut();
// Reset UI state or reload
}
```

`signIn()` returns the new `Identity` directly. It rejects if the user closes the popup or authentication fails, so wrap the call in `try`/`catch` instead of relying on success/error callbacks.

### One-click OpenID sign-in

To skip the Internet Identity authentication-method screen and send the user straight to a specific OpenID provider, pass `openIdProvider` to the constructor. Supported values are `'google'`, `'apple'`, and `'microsoft'`:

```javascript
const authClient = new AuthClient({
identityProvider: getIdentityProviderUrl(),
openIdProvider: "google",
});
```

The rest of the flow (`signIn`, `getIdentity`, `signOut`) is unchanged.

### 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:
After sign-in, 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) {
Expand All @@ -138,6 +149,84 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) {
`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.
:::

### Requesting identity attributes

When a backend canister needs more than just the user's principal (for example, a verified email address), Internet Identity can return signed attributes alongside the delegation. Your backend issues a nonce scoped to a specific action; the frontend requests the attributes during sign-in; the backend verifies the bundle when the user calls the protected method.

**Why a backend-issued nonce?** Tying attributes to a canister-issued nonce prevents replay: an intercepted bundle cannot be reused for a different action, on a different user, or after that action expires. The nonce must originate from the canister, not the frontend.

```typescript
import { AuthClient } from "@icp-sdk/auth/client";
import { AttributesIdentity } from "@icp-sdk/core/identity";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
import { Principal } from "@icp-sdk/core/principal";

async function registerWithEmail() {
// 1. Backend issues a nonce scoped to this registration
const anonymousAgent = await HttpAgent.create();
const backend = Actor.createActor(backendIdl, {
agent: anonymousAgent,
canisterId,
});
const nonce = await backend.registerBegin();

// 2. Run sign-in and the attribute request in parallel.
// The user sees a single Internet Identity interaction.
const signInPromise = authClient.signIn();
const attributesPromise = authClient.requestAttributes({
keys: ["email"],
nonce,
});

const identity = await signInPromise;
const { data, signature } = await attributesPromise;

// 3. Wrap the identity so the signed attributes travel with each call
const identityWithAttributes = new AttributesIdentity({
inner: identity,
attributes: { data, signature },
// The Internet Identity backend canister ID is the attribute signer
signer: { canisterId: Principal.fromText("rdmx6-jaaaa-aaaaa-aaadq-cai") },
});

// 4. Call the protected method. The backend verifies the nonce, origin,
// and timestamp, then reads the email.
const agent = await HttpAgent.create({ identity: identityWithAttributes });
const app = Actor.createActor(appIdl, { agent, canisterId });
await app.registerFinish();
}
```

Each signed attribute bundle carries three implicit fields the backend should verify:

- `implicit:nonce`: matches the canister-issued nonce, preventing replay across actions and users.
- `implicit:origin`: the requesting frontend origin, so a malicious dapp cannot forward attributes to a different backend.
- `implicit:issued_at_timestamp_ns`: issuance time, letting the canister reject stale bundles even when the nonce is still valid.

Attributes can also be requested after sign-in, for example to link an email to an existing account. The pattern is the same: the backend issues a nonce for that action, the frontend calls `requestAttributes`, and the backend verifies the result.

#### OpenID-scoped attributes

When using one-click OpenID sign-in, attributes can be scoped to the provider. The user authenticates and shares attributes in a single step, with no extra prompt:

```typescript
import { AuthClient, scopedKeys } from "@icp-sdk/auth/client";

const authClient = new AuthClient({
identityProvider: getIdentityProviderUrl(),
openIdProvider: "google",
});

const nonce = await backend.registerBegin();
const signInPromise = authClient.signIn();
// Requests name, email, and verified_email from the Google account
// linked to the user's Internet Identity.
const attributesPromise = authClient.requestAttributes({
keys: scopedKeys({ openIdProvider: "google" }),
nonce,
});
```

## 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.
Expand Down Expand Up @@ -224,6 +313,141 @@ async fn protected_async_action() -> String {
}
```

### Read identity attributes

When the frontend wraps an identity with `AttributesIdentity`, every call carries a verified attribute bundle. Read it on the backend with `msg_caller_info_data` (Rust) or `Prim.callerInfoData` (Motoko). Always verify the signer first: by itself the bundle is signed by *some* canister, and any canister could have signed an arbitrary one. Trust it only when the signer is the Internet Identity backend (`rdmx6-jaaaa-aaaaa-aaadq-cai`).

The bundle is Candid-encoded as an [ICRC-3 Value](../../references/internet-identity-spec.md) `Map` with three implicit fields plus the keys you requested:

- `implicit:nonce`: must equal a nonce your canister issued for this user and action.
- `implicit:origin`: must equal a trusted frontend origin.
- `implicit:issued_at_timestamp_ns`: reject if too old (a few minutes is typical).
- Plain attribute keys (e.g., `"email"`) for default-scope attributes; OpenID-scoped keys (e.g., `"openid:https://accounts.google.com:email"`) when the frontend used `scopedKeys`.

<Tabs syncKey="lang">
<TabItem label="Motoko">

```motoko
import Prim "mo:prim";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";

persistent actor {
let iiPrincipal = Principal.fromText("rdmx6-jaaaa-aaaaa-aaadq-cai");

type Icrc3Value = {
#Nat : Nat;
#Int : Int;
#Blob : Blob;
#Text : Text;
#Array : [Icrc3Value];
#Map : [(Text, Icrc3Value)];
};

func lookupText(entries : [(Text, Icrc3Value)], key : Text) : ?Text {
for ((k, v) in entries.vals()) {
if (k == key) {
switch v { case (#Text t) { return ?t }; case _ {} };
};
};
null;
};

// Returns the verified attribute map, trapping if the signer is not II.
func iiAttributes() : [(Text, Icrc3Value)] {
let signer = Prim.callerInfoSigner<system>();
if (signer.size() == 0 or Principal.fromBlob(signer) != iiPrincipal) {
Runtime.trap("Untrusted attribute signer");
};
let data = Prim.callerInfoData<system>();
let ?value : ?Icrc3Value = from_candid (data) else Runtime.trap("invalid attribute bundle");
let #Map(entries) = value else Runtime.trap("expected attribute map");
entries
};

public shared ({ caller }) func registerFinish() : async Text {
if (Principal.isAnonymous(caller)) Runtime.trap("Anonymous caller not allowed");
let entries = iiAttributes();

let ?origin = lookupText(entries, "implicit:origin") else Runtime.trap("missing origin");
if (origin != "https://your-app.icp0.io") Runtime.trap("Wrong origin");

// Compare implicit:nonce to the nonce you minted in registerBegin (omitted for brevity)
// and check implicit:issued_at_timestamp_ns is within your freshness window.

let ?email = lookupText(entries, "email") else Runtime.trap("missing email");
"Registered " # Principal.toText(caller) # " with email " # email
};
};
```

</TabItem>
<TabItem label="Rust">

```rust
use candid::{decode_one, CandidType, Deserialize, Principal};
use ic_cdk::api::{msg_caller, msg_caller_info_data, msg_caller_info_signer};
use ic_cdk::update;

const II_PRINCIPAL: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai";

#[derive(CandidType, Deserialize)]
enum Icrc3Value {
Nat(candid::Nat),
Int(candid::Int),
Blob(Vec<u8>),
Text(String),
Array(Vec<Icrc3Value>),
Map(Vec<(String, Icrc3Value)>),
}

fn lookup_text<'a>(entries: &'a [(String, Icrc3Value)], key: &str) -> Option<&'a str> {
entries.iter().find_map(|(k, v)| match v {
Icrc3Value::Text(s) if k == key => Some(s.as_str()),
_ => None,
})
}

// Returns the verified attribute entries, trapping if the signer is not II.
fn ii_attributes() -> Vec<(String, Icrc3Value)> {
let trusted = Principal::from_text(II_PRINCIPAL).unwrap();
if msg_caller_info_signer() != Some(trusted) {
ic_cdk::trap("Untrusted attribute signer");
}
let bundle = msg_caller_info_data();
let value: Icrc3Value = decode_one(&bundle).unwrap_or_else(|_| ic_cdk::trap("invalid attribute bundle"));
match value {
Icrc3Value::Map(entries) => entries,
_ => ic_cdk::trap("expected attribute map"),
}
}

#[update]
fn register_finish() -> String {
let caller = msg_caller();
if caller == Principal::anonymous() { ic_cdk::trap("Anonymous caller not allowed"); }
let entries = ii_attributes();

let origin = lookup_text(&entries, "implicit:origin")
.unwrap_or_else(|| ic_cdk::trap("missing origin"));
if origin != "https://your-app.icp0.io" { ic_cdk::trap("Wrong origin"); }

// Compare implicit:nonce to the nonce you minted in register_begin (omitted for brevity)
// and check implicit:issued_at_timestamp_ns is within your freshness window.

let email = lookup_text(&entries, "email")
.unwrap_or_else(|| ic_cdk::trap("missing email"));
format!("Registered {} with email {}", caller, email)
}
```

</TabItem>
</Tabs>

:::tip[Storing the nonce]
Mint the nonce in your `registerBegin` (or equivalent) method and persist it in stable memory keyed by the user's principal and the action name. Mark it consumed in `registerFinish` so a bundle cannot be replayed. Use a short freshness window so abandoned attempts age out.
:::

## Local development

Start the local network and deploy. With `ii: true` in your `icp.yaml`, icp-cli deploys a local Internet Identity canister automatically:
Expand Down Expand Up @@ -291,13 +515,12 @@ To keep principals consistent across your own custom domains, configure **altern
]
```

3. **On the alternative origin (B):** Set the `derivationOrigin` in your login call to point back to the primary origin:
3. **On the alternative origin (B):** Set the `derivationOrigin` on the `AuthClient` constructor to point back to the primary origin:

```javascript
authClient.login({
const authClient = new AuthClient({
identityProvider: "https://id.ai",
derivationOrigin: "https://xxxxx.icp0.io", // primary origin A
onSuccess: () => { /* ... */ },
});
```

Expand All @@ -309,11 +532,13 @@ For full details, see the [Internet Identity specification](../../references/int

- **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.
- **Not awaiting `signIn()` or skipping the `try`/`catch`**: `authClient.signIn()` returns a promise that rejects when the user closes the popup or authentication fails. Without `await` and a `catch`, those 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.
- **Generating the attribute nonce on the frontend**: a frontend-generated nonce defeats the anti-replay guarantee. The nonce passed to `requestAttributes` must come from a backend canister call so the canister can later verify that the bundle's `implicit:nonce` matches an action it actually started.
- **Reading attribute data without verifying the signer**: `msg_caller_info_data` (`Prim.callerInfoData` in Motoko) returns whatever bundle the caller provided. The IC system checks the signature, not the identity of the signer. If you skip the `msg_caller_info_signer` check (or compare it against the wrong principal), any canister can mint its own bundle and your method will read attacker-controlled values. Verify the signer matches `rdmx6-jaaaa-aaaaa-aaadq-cai` (Internet Identity) before trusting the bundle.

## Next steps

Expand All @@ -325,4 +550,4 @@ For full details, see the [Internet Identity specification](../../references/int

{/* 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 */}
{/* 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); dfinity/icp-js-sdk-docs (public/auth/latest.zip api/client/ — AuthClient, scopedKeys, SignedAttributes, AuthClientCreateOptions; public/core/latest.zip libs/identity/api.md — AttributesIdentity); dfinity/cdk-rs (ic-cdk/src/api.rs); caffeinelabs/motoko (src/prelude/prim.mo callerInfoData/Signer, test/run-drun/caller-info/caller-info.mo) */}
Loading