Skip to content

Commit 60d0bea

Browse files
authored
docs: update Internet Identity guide for new AuthClient API (v7.0.0) (#193)
1 parent 3005d0f commit 60d0bea

4 files changed

Lines changed: 261 additions & 36 deletions

File tree

.sources/VERSIONS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ ic-pub-key v1.0.1
5252
icp-cli v0.2.3 caeac37
5353
motoko v1.6.0 b818df6
5454
motoko-core v2.4.0 cd37dbf
55-
cdk-rs ic-cdk v0.19.0 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 80d8234
55+
cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c
5656
candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf
5757
response-verification v3.1.0 18c5a37

.sources/cdk-rs

Submodule cdk-rs updated 89 files

docs/guides/authentication/internet-identity.mdx

Lines changed: 258 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "Internet Identity"
3-
description: "Integrate passkey-based authentication with Internet Identity for frontend login, backend caller verification, and session management"
3+
description: "Integrate passkey-based authentication with Internet Identity for frontend sign-in, backend caller verification, and session management"
44
sidebar:
55
order: 1
66
---
@@ -9,7 +9,7 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
99

1010
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.
1111

12-
This guide covers setting up II authentication end-to-end: configuring your project, adding login to your frontend, and verifying callers in your backend.
12+
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.
1313

1414
## How it works
1515

@@ -46,7 +46,7 @@ npm install @icp-sdk/auth @icp-sdk/core
4646

4747
## Frontend integration
4848

49-
The `AuthClient` from `@icp-sdk/auth` handles the full login flow: opening the II popup, receiving the delegation, and managing session persistence.
49+
The `AuthClient` from `@icp-sdk/auth` handles the full sign-in flow: opening the II popup, receiving the delegation, and managing session persistence.
5050

5151
### Environment detection
5252

@@ -77,50 +77,61 @@ function getIdentityProviderUrl() {
7777
}
7878
```
7979

80-
### Login, logout, and session check
80+
### Sign in, sign out, and session check
8181

82-
Create a single `AuthClient` instance on page load and reuse it for all operations:
82+
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:
8383

8484
```javascript
8585
// Create the auth client (once, on page load)
86-
const authClient = await AuthClient.create();
86+
const authClient = new AuthClient({
87+
identityProvider: getIdentityProviderUrl(),
88+
});
8789

8890
// Check for existing session
89-
const isAuthenticated = await authClient.isAuthenticated();
90-
if (isAuthenticated) {
91-
const identity = authClient.getIdentity();
91+
if (authClient.isAuthenticated()) {
92+
const identity = await authClient.getIdentity();
9293
// Restore session: create agent and actor with this identity
9394
}
9495

95-
// Login
96-
async function login() {
97-
return new Promise((resolve, reject) => {
98-
authClient.login({
99-
identityProvider: getIdentityProviderUrl(),
96+
// Sign in
97+
async function signIn() {
98+
try {
99+
const identity = await authClient.signIn({
100100
maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours
101-
onSuccess: () => {
102-
const identity = authClient.getIdentity();
103-
console.log("Logged in as:", identity.getPrincipal().toText());
104-
resolve(identity);
105-
},
106-
onError: (error) => {
107-
console.error("Login failed:", error);
108-
reject(error);
109-
},
110101
});
111-
});
102+
console.log("Signed in as:", identity.getPrincipal().toText());
103+
return identity;
104+
} catch (error) {
105+
console.error("Sign-in failed:", error);
106+
throw error;
107+
}
112108
}
113109

114-
// Logout
115-
async function logout() {
116-
await authClient.logout();
110+
// Sign out
111+
async function signOut() {
112+
await authClient.signOut();
117113
// Reset UI state or reload
118114
}
119115
```
120116

117+
`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.
118+
119+
### One-click OpenID sign-in
120+
121+
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'`:
122+
123+
```javascript
124+
const authClient = new AuthClient({
125+
identityProvider: getIdentityProviderUrl(),
126+
openIdProvider: "google",
127+
});
128+
```
129+
130+
The rest of the flow (`signIn`, `getIdentity`, `signOut`) is unchanged.
131+
121132
### Create an authenticated agent
122133

123-
After login, create an `HttpAgent` using the delegation identity. The agent signs all subsequent canister calls with the user's delegated key:
134+
After sign-in, create an `HttpAgent` using the delegation identity. The agent signs all subsequent canister calls with the user's delegated key:
124135

125136
```javascript
126137
async function createAuthenticatedActor(identity, canisterId, idlFactory) {
@@ -138,6 +149,84 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) {
138149
`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.
139150
:::
140151
152+
### Requesting identity attributes
153+
154+
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.
155+
156+
**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.
157+
158+
```typescript
159+
import { AuthClient } from "@icp-sdk/auth/client";
160+
import { AttributesIdentity } from "@icp-sdk/core/identity";
161+
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
162+
import { Principal } from "@icp-sdk/core/principal";
163+
164+
async function registerWithEmail() {
165+
// 1. Backend issues a nonce scoped to this registration
166+
const anonymousAgent = await HttpAgent.create();
167+
const backend = Actor.createActor(backendIdl, {
168+
agent: anonymousAgent,
169+
canisterId,
170+
});
171+
const nonce = await backend.registerBegin();
172+
173+
// 2. Run sign-in and the attribute request in parallel.
174+
// The user sees a single Internet Identity interaction.
175+
const signInPromise = authClient.signIn();
176+
const attributesPromise = authClient.requestAttributes({
177+
keys: ["email"],
178+
nonce,
179+
});
180+
181+
const identity = await signInPromise;
182+
const { data, signature } = await attributesPromise;
183+
184+
// 3. Wrap the identity so the signed attributes travel with each call
185+
const identityWithAttributes = new AttributesIdentity({
186+
inner: identity,
187+
attributes: { data, signature },
188+
// The Internet Identity backend canister ID is the attribute signer
189+
signer: { canisterId: Principal.fromText("rdmx6-jaaaa-aaaaa-aaadq-cai") },
190+
});
191+
192+
// 4. Call the protected method. The backend verifies the nonce, origin,
193+
// and timestamp, then reads the email.
194+
const agent = await HttpAgent.create({ identity: identityWithAttributes });
195+
const app = Actor.createActor(appIdl, { agent, canisterId });
196+
await app.registerFinish();
197+
}
198+
```
199+
200+
Each signed attribute bundle carries three implicit fields the backend should verify:
201+
202+
- `implicit:nonce`: matches the canister-issued nonce, preventing replay across actions and users.
203+
- `implicit:origin`: the requesting frontend origin, so a malicious dapp cannot forward attributes to a different backend.
204+
- `implicit:issued_at_timestamp_ns`: issuance time, letting the canister reject stale bundles even when the nonce is still valid.
205+
206+
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.
207+
208+
#### OpenID-scoped attributes
209+
210+
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:
211+
212+
```typescript
213+
import { AuthClient, scopedKeys } from "@icp-sdk/auth/client";
214+
215+
const authClient = new AuthClient({
216+
identityProvider: getIdentityProviderUrl(),
217+
openIdProvider: "google",
218+
});
219+
220+
const nonce = await backend.registerBegin();
221+
const signInPromise = authClient.signIn();
222+
// Requests name, email, and verified_email from the Google account
223+
// linked to the user's Internet Identity.
224+
const attributesPromise = authClient.requestAttributes({
225+
keys: scopedKeys({ openIdProvider: "google" }),
226+
nonce,
227+
});
228+
```
229+
141230
## Backend authentication
142231
143232
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.
@@ -224,6 +313,141 @@ async fn protected_async_action() -> String {
224313
}
225314
```
226315
316+
### Read identity attributes
317+
318+
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`).
319+
320+
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:
321+
322+
- `implicit:nonce`: must equal a nonce your canister issued for this user and action.
323+
- `implicit:origin`: must equal a trusted frontend origin.
324+
- `implicit:issued_at_timestamp_ns`: reject if too old (a few minutes is typical).
325+
- 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`.
326+
327+
<Tabs syncKey="lang">
328+
<TabItem label="Motoko">
329+
330+
```motoko
331+
import Prim "mo:prim";
332+
import Principal "mo:core/Principal";
333+
import Runtime "mo:core/Runtime";
334+
335+
persistent actor {
336+
let iiPrincipal = Principal.fromText("rdmx6-jaaaa-aaaaa-aaadq-cai");
337+
338+
type Icrc3Value = {
339+
#Nat : Nat;
340+
#Int : Int;
341+
#Blob : Blob;
342+
#Text : Text;
343+
#Array : [Icrc3Value];
344+
#Map : [(Text, Icrc3Value)];
345+
};
346+
347+
func lookupText(entries : [(Text, Icrc3Value)], key : Text) : ?Text {
348+
for ((k, v) in entries.vals()) {
349+
if (k == key) {
350+
switch v { case (#Text t) { return ?t }; case _ {} };
351+
};
352+
};
353+
null;
354+
};
355+
356+
// Returns the verified attribute map, trapping if the signer is not II.
357+
func iiAttributes() : [(Text, Icrc3Value)] {
358+
let signer = Prim.callerInfoSigner<system>();
359+
if (signer.size() == 0 or Principal.fromBlob(signer) != iiPrincipal) {
360+
Runtime.trap("Untrusted attribute signer");
361+
};
362+
let data = Prim.callerInfoData<system>();
363+
let ?value : ?Icrc3Value = from_candid (data) else Runtime.trap("invalid attribute bundle");
364+
let #Map(entries) = value else Runtime.trap("expected attribute map");
365+
entries
366+
};
367+
368+
public shared ({ caller }) func registerFinish() : async Text {
369+
if (Principal.isAnonymous(caller)) Runtime.trap("Anonymous caller not allowed");
370+
let entries = iiAttributes();
371+
372+
let ?origin = lookupText(entries, "implicit:origin") else Runtime.trap("missing origin");
373+
if (origin != "https://your-app.icp0.io") Runtime.trap("Wrong origin");
374+
375+
// Compare implicit:nonce to the nonce you minted in registerBegin (omitted for brevity)
376+
// and check implicit:issued_at_timestamp_ns is within your freshness window.
377+
378+
let ?email = lookupText(entries, "email") else Runtime.trap("missing email");
379+
"Registered " # Principal.toText(caller) # " with email " # email
380+
};
381+
};
382+
```
383+
384+
</TabItem>
385+
<TabItem label="Rust">
386+
387+
```rust
388+
use candid::{decode_one, CandidType, Deserialize, Principal};
389+
use ic_cdk::api::{msg_caller, msg_caller_info_data, msg_caller_info_signer};
390+
use ic_cdk::update;
391+
392+
const II_PRINCIPAL: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai";
393+
394+
#[derive(CandidType, Deserialize)]
395+
enum Icrc3Value {
396+
Nat(candid::Nat),
397+
Int(candid::Int),
398+
Blob(Vec<u8>),
399+
Text(String),
400+
Array(Vec<Icrc3Value>),
401+
Map(Vec<(String, Icrc3Value)>),
402+
}
403+
404+
fn lookup_text<'a>(entries: &'a [(String, Icrc3Value)], key: &str) -> Option<&'a str> {
405+
entries.iter().find_map(|(k, v)| match v {
406+
Icrc3Value::Text(s) if k == key => Some(s.as_str()),
407+
_ => None,
408+
})
409+
}
410+
411+
// Returns the verified attribute entries, trapping if the signer is not II.
412+
fn ii_attributes() -> Vec<(String, Icrc3Value)> {
413+
let trusted = Principal::from_text(II_PRINCIPAL).unwrap();
414+
if msg_caller_info_signer() != Some(trusted) {
415+
ic_cdk::trap("Untrusted attribute signer");
416+
}
417+
let bundle = msg_caller_info_data();
418+
let value: Icrc3Value = decode_one(&bundle).unwrap_or_else(|_| ic_cdk::trap("invalid attribute bundle"));
419+
match value {
420+
Icrc3Value::Map(entries) => entries,
421+
_ => ic_cdk::trap("expected attribute map"),
422+
}
423+
}
424+
425+
#[update]
426+
fn register_finish() -> String {
427+
let caller = msg_caller();
428+
if caller == Principal::anonymous() { ic_cdk::trap("Anonymous caller not allowed"); }
429+
let entries = ii_attributes();
430+
431+
let origin = lookup_text(&entries, "implicit:origin")
432+
.unwrap_or_else(|| ic_cdk::trap("missing origin"));
433+
if origin != "https://your-app.icp0.io" { ic_cdk::trap("Wrong origin"); }
434+
435+
// Compare implicit:nonce to the nonce you minted in register_begin (omitted for brevity)
436+
// and check implicit:issued_at_timestamp_ns is within your freshness window.
437+
438+
let email = lookup_text(&entries, "email")
439+
.unwrap_or_else(|| ic_cdk::trap("missing email"));
440+
format!("Registered {} with email {}", caller, email)
441+
}
442+
```
443+
444+
</TabItem>
445+
</Tabs>
446+
447+
:::tip[Storing the nonce]
448+
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.
449+
:::
450+
227451
## Local development
228452

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

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

296520
```javascript
297-
authClient.login({
521+
const authClient = new AuthClient({
298522
identityProvider: "https://id.ai",
299523
derivationOrigin: "https://xxxxx.icp0.io", // primary origin A
300-
onSuccess: () => { /* ... */ },
301524
});
302525
```
303526

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

310533
- **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.
311534
- **`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.
312-
- **Missing `onSuccess`/`onError` callbacks**: `authClient.login()` requires both. Without them, login failures are silently swallowed.
535+
- **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.
313536
- **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.
314537
- **Passing principal as a string argument**: the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter.
315538
- **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.
316539
- **Creating multiple `AuthClient` instances**: create one on page load and reuse it. Multiple instances cause race conditions with session storage.
540+
- **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.
541+
- **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.
317542

318543
## Next steps
319544

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

326551
{/* TODO: Add Unity native app integration via deep links: see portal native-apps/unity_ii_* */}
327552

328-
{/* 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 */}
553+
{/* 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) */}

0 commit comments

Comments
 (0)