Install
$ agentstack add skill-dfinity-icskills-internet-identity ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Internet Identity Authentication
What This Is
Internet Identity (II) is the Internet Computer's native authentication system. Users authenticate into II-powered apps either with passkeys stored in their devices or through OpenID accounts (e.g., Google, Apple, Microsoft) -- no usernames or passwords required. Each user gets a unique principal per app, preventing cross-app tracking.
Prerequisites
@icp-sdk/auth(>= 7.0.0),@icp-sdk/core(>= 5.3.0) (AttributesIdentitywas added in core v5.3.0)- For the Motoko backend example:
mo:identity-attributes>= 0.4.0 (mops) — the mixin that injects the two sign-in methods and verifies the bundle for you. It pulls inmo:core>= 2.5.0 and requiresmoc>= 1.6.0 for theincludemixin.
Canister IDs
| Canister | ID | URL | Purpose | |----------|------------|-----|---------| | Internet Identity (backend) | rdmx6-jaaaa-aaaaa-aaadq-cai | | Manages user keys and authentication logic | | Internet Identity (frontend) | uqzsh-gqaaa-aaaaq-qaada-cai | https://id.ai | Serves the II web app; identity provider URL points here |
Mistakes That Break Your Build
- Using the wrong II URL for the environment. The identity provider URL must point to the frontend canister (
uqzsh-gqaaa-aaaaq-qaada-cai), not the backend. Mainnet useshttps://id.ai/authorize. Local-only II (whenii: trueis set inicp.yaml) useshttp://id.ai.localhost:8000/authorize. Both canister IDs are well-known and identical on mainnet and local replicas — hardcode them rather than doing a dynamic lookup.
- Forgetting
/authorizein theidentityProviderURL. In@icp-sdk/auth7.x the URL is used verbatim; the client does not append/authorizefor you (it did in 5.x). Passinghttps://id.aiopens the II home page in the popup and never returns a delegation — the login button appears to do nothing. Always include the/authorizepath.
- Setting delegation expiry too long. Maximum delegation expiry is 30 days (2592000000000_000 nanoseconds). Longer values are silently clamped, which causes confusing session behavior. Use 8 hours for normal apps, 30 days maximum for "remember me" flows.
- Not awaiting
signIn()or skipping thetry/catch.authClient.signIn()returns a promise that rejects when the user closes the popup or authentication fails. Withoutawaitand acatch, those failures are silently swallowed.
- Using
shouldFetchRootKeyorfetchRootKey()instead of theic_envcookie. Theic_envcookie (set by the asset canister or the Vite dev server) already contains the root key asIC_ROOT_KEY. Pass it via therootKeyoption toHttpAgent.create()— this works in both local and production environments without environment branching. See the icp-cli skill'sreferences/binding-generation.mdfor the pattern. Never callfetchRootKey()— it fetches the root key from the replica at runtime, which lets a man-in-the-middle substitute a fake key on mainnet.
- Getting
2vxsx-faeas the principal after sign-in. That is the anonymous principal -- it means authentication silently failed. Common causes: wrongidentityProviderURL passed to theAuthClientconstructor (especially missing/authorize), an unhandled rejection fromsignIn(), or readinggetIdentity()beforesignIn()resolved.
- Passing principal as string to backend. The
AuthClientgives you anIdentityobject. Backend canister methods receive the caller principal automatically via the IC protocol -- you do not pass it as a function argument. The caller principal is available on the backend viashared(msg) { msg.caller }in Motoko oric_cdk::api::msg_caller()in Rust. For backend access control patterns, see the canister-security skill.
- Adding
derivationOriginorii-alternative-originsto handleicp0.iovsic0.app. Internet Identity automatically rewritesicp0.iotoic0.appduring delegation, so both domains produce the same principal. Do not addderivationOriginorii-alternative-originsconfiguration to handle this — it will break authentication. If a user reports getting a different principal, the cause is almost certainly a different passkey or device, not the domain.
- Generating the attribute nonce on the frontend. The nonce passed to
requestAttributesMUST come from a backend canister call. A frontend-generated nonce defeats replay protection: the canister cannot verify that the bundle'simplicit:nonceis one it actually issued. Have the backend mint and return the nonce from_internet_identity_sign_in_start(themo:identity-attributesmixin provides it in Motoko; you write it in Rust), and check it against the bundle's implicit fields when the user calls_internet_identity_sign_in_finish.
- Reading attribute data without verifying the signer. The IC verifies the signature, not the identity of the signer — any canister can produce a valid bundle. The trusted signer is
rdmx6-jaaaa-aaaaa-aaadq-cai(Internet Identity). The check looks different per language:
- Motoko: use the
mo:identity-attributesmixin.include IdentityAttributes({ onVerified })verifies the signer, origin, nonce, and freshness for you and runsonVerifiedonly on a bundle that passes — configuretrusted_attribute_signersandfrontend_originsinicp.yaml(see "Backend: Reading Identity Attributes"). Don't hand-roll the ICRC-3 decode or the signer check on top ofmo:core/CallerAttributesunless you need behavior the library doesn't cover. - Rust: there is no CDK wrapper yet. Always check
msg_caller_info_signer()against the trusted issuer principal before readingmsg_caller_info_data(). Skipping this lets an attacker canister forge attributes likeemail = "admin@you.com".
- Substituting
{tid}in the Microsoft scoped-key prefix. ThemicrosoftOpenID provider URL is the literal stringhttps://login.microsoftonline.com/{tid}/v2.0—{tid}is part of the URL, not a tenant-ID placeholder you fill in. Bundle keys returned byscopedKeys({ openIdProvider: 'microsoft' })look likeopenid:https://login.microsoftonline.com/{tid}/v2.0:emailexactly, and the backend must look up that literal key. Replacing{tid}with a tenant GUID will silently miss every attribute lookup.
- Treating
emailas verified.emailandverified_emailare distinct keys.
emailis the raw email string from the user's II-linked account. II does not check it. Treat it as user-supplied input.verified_emailis the same email asemail, but only present when the source OpenID provider (e.g., Google) marked it as verified and II surfaced that signal through.
Use verified_email for any access gating (admin allowlists, capability checks). Use email only for soft uses like contact info or mailing lists. Request both for fallback behaviour: both are returned with the same value when the source provider marked the email as verified, only email when it didn't.
Using II during local development
Default: use mainnet II from your local network. Starting with icp-cli >= 0.2.4, the local network (pocket-ic, launched by icp-cli-network-launcher) is configured to trust the mainnet subnet's BLS signatures. Delegations signed by https://id.ai are accepted by your local replica, so both the sign-in flow and authenticated calls to a locally-deployed backend just work — no extra config in icp.yaml, no local II canister to manage, and the UI is the real one your users will see.
Point your frontend at https://id.ai/authorize unconditionally and you're done.
Fallback: deploy II locally
Only use this if you need fully-offline dev or want to test against a specific II build. Add ii: true to the local network in your icp.yaml:
networks:
- name: local
mode: managed
ii: true
This deploys the II canisters automatically when the local network is started. The II frontend will be available at http://id.ai.localhost:8000, and the identityProvider URL becomes http://id.ai.localhost:8000/authorize. No canister entry is needed in your project — II is not part of your project's canisters. For the full icp.yaml canister configuration, see the icp-cli and asset-canister skills.
Frontend: Vanilla JavaScript/TypeScript Sign-In Flow
This is framework-agnostic. Adapt the DOM manipulation to your framework.
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 the root key and canister IDs — works in both local and production.
const canisterEnv = safeGetCanisterEnv();
// Construct once — identityProvider (and optionally derivationOrigin or
// openIdProvider for one-click sign-in: 'google' | 'apple' | 'microsoft')
// are configured at construction time, not per sign-in. Always include the
// `/authorize` path — the client uses the URL verbatim in 7.x.
//
// Use mainnet II even from local dev: pocket-ic (icp-cli >= 0.2.4) trusts
// mainnet subnet signatures. Override to http://id.ai.localhost:8000/authorize
// only if you have `ii: true` in icp.yaml and want fully-offline dev.
const authClient = new AuthClient({
identityProvider: "https://id.ai/authorize",
});
// Sign in: signIn() returns the new Identity directly and rejects if the user
// closes the popup or authentication fails.
async function signIn() {
try {
const identity = await authClient.signIn({
maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours in nanoseconds
});
console.log("Signed in as:", identity.getPrincipal().toText());
return identity;
} catch (error) {
console.error("Sign-in failed:", error);
throw error;
}
}
// Sign out
async function signOut() {
await authClient.signOut();
// Optionally reload or reset UI state
}
// Create an authenticated agent and actor.
// Uses rootKey from the ic_env cookie — no shouldFetchRootKey or environment branching needed.
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 });
}
// Initialization — wraps async setup in a function so this code works with
// any bundler target (Vite defaults to es2020 which lacks top-level await).
async function init() {
// isAuthenticated() is sync; getIdentity() is async.
if (authClient.isAuthenticated()) {
const identity = await authClient.getIdentity();
const actor = await createAuthenticatedActor(identity, canisterId, idlFactory);
// Use actor to call backend methods
}
}
init();
Frontend: Requesting Identity Attributes
When the backend needs more than the user's principal (e.g., a verified email), Internet Identity can return signed attributes alongside the delegation. The flow is a two-method handshake on the backend: _internet_identity_sign_in_start mints a nonce, and _internet_identity_sign_in_finish verifies the bundle. In Motoko the mo:identity-attributes mixin provides both methods; in Rust you implement them by hand (see "Backend: Reading Identity Attributes"). The frontend below is identical against either backend.
Available attribute keys
requestAttributes({ keys, nonce }) requires both keys and nonce: there is no default key set, you must pass an explicit list. The keys II currently accepts are:
| Key | What it IS | When to use | |---|---|---| | name | The user's display name from the II-linked account. | Personalisation in the UI. | | email | The raw email string from the user's II-linked account. II does not check it. Treat as user-supplied input. | Mailing-list signups, contact email, anything where you don't gate access on the email. | | verified_email | The same email as email, but only present when the source OpenID provider (e.g., Google) marked it as verified and II surfaced that signal. The provider's verification is what makes it trustworthy. | Access gating (e.g. an admin allowlist by email). Treat this as the only trustworthy email for authorisation. |
Request both email and verified_email if you want fallback behaviour: when the source provider marked the email as verified, both keys are present with the same value; when it didn't, only email is returned.
scopedKeys({ openIdProvider, keys? }) rewrites the keys above into provider-scoped keys of the form openid::, so II returns the values from the linked OpenID account directly (with implicit consent, no extra prompt). Provider URLs:
| Provider | URL prefix in the bundle keys | |---|---| | 'google' | openid:https://accounts.google.com: | | 'apple' | openid:https://appleid.apple.com: | | 'microsoft' | openid:https://login.microsoftonline.com/{tid}/v2.0: (the {tid} part is literal: do not substitute a tenant ID into it) |
The keys argument to scopedKeys is optional and defaults to ['name', 'email', 'verified_email']. (requestAttributes itself has no default; the scopedKeys helper just builds the array you then pass to it.) Examples:
scopedKeys({ openIdProvider: 'google' })→['openid:https://accounts.google.com:name', 'openid:https://accounts.google.com:email', 'openid:https://accounts.google.com:verified_email']scopedKeys({ openIdProvider: 'google', keys: ['email'] })→['openid:https://accounts.google.com:email']
The same email vs verified_email rule applies to scoped keys: use the verified variant when the email gates access.
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";
const II_PRINCIPAL = "rdmx6-jaaaa-aaaaa-aaadq-cai";
// `idl` and `canisterId` are your backend's interface factory and ID. The
// backend exposes _internet_identity_sign_in_start / _internet_identity_sign_in_finish.
async function signInWithAttributes(authClient, canisterId, idl) {
// Anonymous handle, used only to mint the nonce.
const anonymousAgent = await HttpAgent.create();
const anonymousActor = Actor.createActor(idl, { agent: anonymousAgent, canisterId });
// Mint the nonce, sign in, and request attributes in parallel. Passing the
// nonce as a promise lets requestAttributes start before it resolves, so the
// user still sees a single Internet Identity interaction. A frontend-generated
// nonce would defeat replay protection — see Mistake #9.
const noncePromise = anonymousActor._internet_identity_sign_in_start();
const signInPromise = authClient.signIn({
maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours in nanoseconds
});
const attributesPromise = authClient.requestAttributes({
keys: ["name", "verified_email"], // library reads verified_email for its email field
nonce: noncePromise,
});
const identity = await signInPromise;
const attributes = await attributesPromise;
// Wrap the identity so the signed bundle travels as sender_info on each call.
const verifiedAgent = await HttpAgent.create({
identity: new AttributesIdentity({
inner: identity,
attributes,
// The Internet Identity backend canister is the trusted attribute signer.
signer: { canisterId: Principal.fromText(II_PRINCIPAL) },
}),
});
const verifiedActor = Actor.createActor(idl, { agent: verifiedAgent, canisterId });
// The backend verifies signer, origin, nonce, and freshness, then runs its
// onVerified logic. Returns { ok } on success, { err } otherwise.
const result = await verifiedActor._internet_identity_sign_in_finish();
if ("err" in result
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [dfinity](https://github.com/dfinity)
- **Source:** [dfinity/icskills](https://github.com/dfinity/icskills)
- **License:** Apache-2.0
- **Homepage:** https://skills.internetcomputer.org
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.