internet-identity
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInternet Identity Authentication
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 thorugh OpenID accounts (e.g., Google, Apple, Microsoft) -- no login or passwords required. Each user gets a unique principal per app, preventing cross-app tracking.
Internet Identity (II) 是Internet Computer的原生认证系统。用户可通过设备中存储的passkey或OpenID账户(如Google、Apple、Microsoft)登录支持II的应用——无需用户名或密码。每个用户在每个应用中都拥有唯一的主体,可防止跨应用追踪。
Prerequisites
前置条件
- Frontend: ,
@icp-sdk/auth@icp-sdk/core - For Motoko: package manager,
mopsin mops.tomlcore = "2.0.0" - For Rust:
ic-cdk >= 0.19
- 前端:、
@icp-sdk/auth@icp-sdk/core - Motoko开发:包管理器,mops.toml中配置
mopscore = "2.0.0" - Rust开发:
ic-cdk >= 0.19
Canister IDs
容器ID
| Canister | ID | URL | Purpose |
|---|---|---|---|
| Internet Identity | | | Stores and manages user keys, serves the II web app over HTTPS |
| 容器 | ID | URL | 用途 |
|---|---|---|---|
| Internet Identity | | | 存储并管理用户密钥,通过HTTPS提供II网页应用服务 |
Mistakes That Break Your Build
导致构建失败的常见错误
-
Using the wrong II URL for the environment. Local development must point to(this canister ID may be different from mainnet). Mainnet must use
http://<local-ii-canister-id>.localhost:8000. Hardcoding one breaks the other. The local II canister ID is assigned dynamically when you runhttps://id.ai-- read it from theicp deploy internet_identitycookie usingic_envfromsafeGetCanisterEnv(see the icp-cli skill for details on canister environment variables).@icp-sdk/core/agent/canister-env -
Setting delegation expiry too long. Maximum delegation expiry is 30 days (2_592_000_000_000_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 handling auth callbacks. Thecall requires
authClient.login()andonSuccesscallbacks. Without them, login failures are silently swallowed.onError -
Defensive practice: bindbefore
msg_caller()in Rust. The current ic-cdk executor preserves the caller across.awaitpoints, but capturing it early guards against future executor changes. Always bind.awaitat the top of async update functions.let caller = ic_cdk::api::msg_caller(); -
Passing principal as string to backend. Thegives you an
AuthClientobject. Backend canister methods receive the caller principal automatically via the IC protocol -- you do not pass it as a function argument. UseIdentityin Motoko orshared(msg) { msg.caller }in Rust.ic_cdk::api::msg_caller() -
Not callingin local development. Without this, certificate verification fails on localhost. Never call it in production -- it's a security risk on mainnet.
agent.fetchRootKey() -
Storing auth state inwithout stable storage (Rust) --
thread_local!is heap memory, wiped on every canister upgrade. Usethread_local! { RefCell<T> }fromStableCellfor any state that must persist across upgrades, especially ownership/auth data.ic-stable-structures
-
使用错误的II环境URL。本地开发必须指向(该容器ID可能与主网不同)。主网必须使用
http://<local-ii-canister-id>.localhost:8000。硬编码其中一个会导致另一个环境无法正常工作。本地II容器ID是在运行https://id.ai时动态分配的——可使用icp deploy internet_identity中的@icp-sdk/core/agent/canister-env从safeGetCanisterEnvcookie中读取(关于容器环境变量的详细信息请参考icp-cli技能文档)。ic_env -
设置过长的委托有效期。委托的最大有效期为30天(2_592_000_000_000_000纳秒)。超过该值的设置会被自动截断,导致会话行为异常。普通应用建议设置为8小时,“记住我”场景最多设置为30天。
-
未处理认证回调。调用时必须传入
authClient.login()和onSuccess回调函数。如果不设置,登录失败的信息会被静默忽略。onError -
Rust防御性实践:在前绑定
.await。当前ic-cdk执行器会在msg_caller()节点保留调用方信息,但提前捕获该信息可防范未来执行器变更带来的问题。在异步更新函数顶部务必添加.await。let caller = ic_cdk::api::msg_caller(); -
将主体以字符串形式传递给后端。会返回一个
AuthClient对象。后端容器方法会通过IC协议自动获取调用方主体——无需将其作为函数参数传递。Motoko中使用Identity,Rust中使用shared(msg) { msg.caller }即可。ic_cdk::api::msg_caller() -
本地开发时未调用。如果不调用该方法,本地环境下的证书验证会失败。注意:生产环境绝对不能调用该方法——这会给主网带来安全风险。
agent.fetchRootKey() -
在Rust中使用存储认证状态但未搭配稳定存储——
thread_local!存储在堆内存中,容器升级时会被清空。对于需要跨版本持久化的状态(尤其是所有权/认证数据),请使用thread_local! { RefCell<T> }中的ic-stable-structures。StableCell
Implementation
实现步骤
icp.yaml Configuration
icp.yaml配置
For local development, you just need to add the property to the local network to enable Internet Identity.
iiHere's an example icp.yaml configuration (assume that the canister is generated using using the template):
frontendicp newstatic-websiteyaml
undefined本地开发时,只需在本地网络配置中添加属性即可启用Internet Identity。
ii以下是icp.yaml配置示例(假设容器是使用命令的模板生成的):
frontendicp newstatic-websiteyaml
undefinedyaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/icp-yaml-schema.json
yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/icp-yaml-schema.json
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
build:
# Install the dependencies
# Eventually you might want to use to lock your dependencies - npm install - npm run build dir: dist
npm ci
networks:
- name: local mode: managed ii: true
<!-- Reviewed till here -->canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
build:
# 安装依赖
# 最终你可能会使用来锁定依赖版本 - npm install - npm run build dir: dist
npm ci
networks:
- name: local mode: managed ii: true
<!-- Reviewed till here -->Frontend: Vanilla JavaScript/TypeScript Login Flow
前端:原生JavaScript/TypeScript登录流程
This is framework-agnostic. Adapt the DOM manipulation to your framework.
javascript
import { AuthClient } from "@icp-sdk/auth/client";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
// 1. Create the auth client
const authClient = await AuthClient.create();
// 2. Determine II URL based on environment
// The local II canister gets a different canister ID each time you deploy it.
// Pass it via an environment variable at build time (e.g., Vite: import.meta.env.VITE_II_CANISTER_ID).
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).
// Read it at runtime using @icp-sdk/core:
// import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
// const canisterEnv = safeGetCanisterEnv();
// const iiCanisterId = canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"];
const iiCanisterId = canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"]
?? "be2us-64aaa-aaaaa-qaabq-cai"; // fallback -- replace with your actual local II canister ID
return `http://${iiCanisterId}.localhost:8000`;
}
return "https://id.ai";
}
// 3. Login
async function login() {
return new Promise((resolve, reject) => {
authClient.login({
identityProvider: getIdentityProviderUrl(),
maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours in nanoseconds
onSuccess: () => {
const identity = authClient.getIdentity();
const principal = identity.getPrincipal().toText();
console.log("Logged in as:", principal);
resolve(identity);
},
onError: (error) => {
console.error("Login failed:", error);
reject(error);
},
});
});
}
// 4. Create an authenticated agent and actor
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 });
}
// 5. Logout
async function logout() {
await authClient.logout();
// Optionally reload or reset UI state
}
// 6. Check if already authenticated (on page load)
const isAuthenticated = await authClient.isAuthenticated();
if (isAuthenticated) {
const identity = authClient.getIdentity();
// Restore session -- create actor, update UI
}该实现与框架无关,可根据你的框架调整DOM操作逻辑。
javascript
import { AuthClient } from "@icp-sdk/auth/client";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
// 1. 创建认证客户端
const authClient = await AuthClient.create();
// 2. 根据环境确定II的URL
// 本地II容器的ID每次部署都会变化。
// 构建时可通过环境变量传递(例如Vite中使用import.meta.env.VITE_II_CANISTER_ID)。
function getIdentityProviderUrl() {
const host = window.location.hostname;
const isLocal = host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost");
if (isLocal) {
// icp-cli会通过asset容器设置的ic_env cookie注入容器ID。
// 运行时可通过@icp-sdk/core读取:
// import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
// const canisterEnv = safeGetCanisterEnv();
// const iiCanisterId = canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"];
const iiCanisterId = canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"]
?? "be2us-64aaa-aaaaa-qaabq-cai"; // 备用值——请替换为你实际的本地II容器ID
return `http://${iiCanisterId}.localhost:8000`;
}
return "https://id.ai";
}
// 3. 登录
async function login() {
return new Promise((resolve, reject) => {
authClient.login({
identityProvider: getIdentityProviderUrl(),
maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8小时(纳秒单位)
onSuccess: () => {
const identity = authClient.getIdentity();
const principal = identity.getPrincipal().toText();
console.log("Logged in as:", principal);
resolve(identity);
},
onError: (error) => {
console.error("Login failed:", error);
reject(error);
},
});
});
}
// 4. 创建已认证的agent和actor
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 });
}
// 5. 登出
async function logout() {
await authClient.logout();
// 可选:重新加载页面或重置UI状态
}
// 6. 页面加载时检查是否已认证
const isAuthenticated = await authClient.isAuthenticated();
if (isAuthenticated) {
const identity = authClient.getIdentity();
// 恢复会话——创建actor,更新UI
}Backend: Motoko
后端:Motoko实现
Requires installing the Mops Motoko package manager:
sh
npm install -g ic-mopsmotoko
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
persistent actor {
// Owner/admin principal
var owner : ?Principal = null;
// Helper: reject anonymous callers
func requireAuth(caller : Principal) : () {
if (Principal.isAnonymous(caller)) {
Runtime.trap("Anonymous principal not allowed. Please authenticate.");
};
};
// Initialize the first authenticated caller as owner
public shared (msg) func initOwner() : async Text {
requireAuth(msg.caller);
switch (owner) {
case (null) {
owner := ?msg.caller;
"Owner set to " # Principal.toText(msg.caller);
};
case (?_existing) {
"Owner already initialized";
};
};
};
// Owner-only endpoint example
public shared (msg) func adminAction() : async Text {
requireAuth(msg.caller);
switch (owner) {
case (?o) {
if (o != msg.caller) {
Runtime.trap("Only the owner can call this function.");
};
"Admin action performed";
};
case (null) {
Runtime.trap("Owner not set. Call initOwner first.");
};
};
};
// Public query: anyone can call, but returns different data for authenticated users
public shared query (msg) func whoAmI() : async Text {
if (Principal.isAnonymous(msg.caller)) {
"You are not authenticated (anonymous)";
} else {
"Your principal: " # Principal.toText(msg.caller);
};
};
// Getting caller principal in shared functions
// ALWAYS use `shared (msg)` or `shared ({ caller })` syntax:
public shared ({ caller }) func protectedEndpoint(data : Text) : async Bool {
requireAuth(caller);
// Use `caller` for authorization checks
true;
};
};需要先安装Mops Motoko包管理器:
sh
npm install -g ic-mopsmotoko
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
persistent actor {
// 所有者/管理员主体
var owner : ?Principal = null;
// 工具函数:拒绝匿名调用方
func requireAuth(caller : Principal) : () {
if (Principal.isAnonymous(caller)) {
Runtime.trap("不允许匿名主体,请先完成认证。");
};
};
// 将第一个已认证的调用方设置为所有者
public shared (msg) func initOwner() : async Text {
requireAuth(msg.caller);
switch (owner) {
case (null) {
owner := ?msg.caller;
"Owner set to " # Principal.toText(msg.caller);
};
case (?_existing) {
"Owner already initialized";
};
};
};
// 仅所有者可调用的接口示例
public shared (msg) func adminAction() : async Text {
requireAuth(msg.caller);
switch (owner) {
case (?o) {
if (o != msg.caller) {
Runtime.trap("只有所有者可以调用此函数。");
};
"Admin action performed";
};
case (null) {
Runtime.trap("所有者未设置,请先调用initOwner。");
};
};
};
// 公共查询接口:任何人都可调用,但对已认证用户返回不同数据
public shared query (msg) func whoAmI() : async Text {
if (Principal.isAnonymous(msg.caller)) {
"You are not authenticated (anonymous)";
} else {
"Your principal: " # Principal.toText(msg.caller);
};
};
// 在共享函数中获取调用方主体
// 务必使用`shared (msg)`或`shared ({ caller })`语法:
public shared ({ caller }) func protectedEndpoint(data : Text) : async Bool {
requireAuth(caller);
// 使用`caller`进行授权校验
true;
};
};Backend: Rust
后端:Rust实现
toml
undefinedtoml
undefinedCargo.toml
Cargo.toml
[package]
name = "ii_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ic-cdk = "0.19"
candid = "0.10"
serde = { version = "1", features = ["derive"] }
ic-stable-structures = "0.7"
```rust
use candid::Principal;
use ic_cdk::{query, update};
use ic_stable_structures::{DefaultMemoryImpl, StableCell};
use std::cell::RefCell;
thread_local! {
// Principal::anonymous() is used as the "not set" sentinel.
// Option<Principal> does not implement Storable, so we store Principal directly.
static OWNER: RefCell<StableCell<Principal, DefaultMemoryImpl>> = RefCell::new(
StableCell::init(DefaultMemoryImpl::default(), Principal::anonymous())
);
}
/// Reject anonymous principal. Call this at the top of every protected endpoint.
fn require_auth() -> Principal {
let caller = ic_cdk::api::msg_caller();
if caller == Principal::anonymous() {
ic_cdk::trap("Anonymous principal not allowed. Please authenticate.");
}
caller
}
#[update]
fn init_owner() -> String {
// Defensive: capture caller before any .await calls.
let caller = require_auth();
OWNER.with(|owner| {
let mut cell = owner.borrow_mut();
let current = *cell.get();
if current == Principal::anonymous() {
cell.set(caller);
format!("Owner set to {}", caller)
} else {
"Owner already initialized".to_string()
}
})
}
#[update]
fn admin_action() -> String {
let caller = require_auth();
OWNER.with(|owner| {
let cell = owner.borrow();
let current = *cell.get();
if current == Principal::anonymous() {
ic_cdk::trap("Owner not set. Call init_owner first.");
} else if current == caller {
"Admin action performed".to_string()
} else {
ic_cdk::trap("Only the owner can call this function.");
}
})
}
#[query]
fn who_am_i() -> String {
let caller = ic_cdk::api::msg_caller();
if caller == Principal::anonymous() {
"You are not authenticated (anonymous)".to_string()
} else {
format!("Your principal: {}", caller)
}
}
// For async functions, capture caller before await as defensive practice:
#[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)
}Rust defensive practice: Bind at the top of async update functions. The current ic-cdk executor preserves caller across points via protected tasks, but capturing it early guards against future executor changes.
let caller = ic_cdk::api::msg_caller();.await[package]
name = "ii_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ic-cdk = "0.19"
candid = "0.10"
serde = { version = "1", features = ["derive"] }
ic-stable-structures = "0.7"
```rust
use candid::Principal;
use ic_cdk::{query, update};
use ic_stable_structures::{DefaultMemoryImpl, StableCell};
use std::cell::RefCell;
thread_local! {
// 使用Principal::anonymous()作为"未设置"的标记值。
// Option<Principal>未实现Storable trait,因此直接存储Principal类型。
static OWNER: RefCell<StableCell<Principal, DefaultMemoryImpl>> = RefCell::new(
StableCell::init(DefaultMemoryImpl::default(), Principal::anonymous())
);
}
/// 拒绝匿名主体。在每个受保护的接口顶部调用此函数。
fn require_auth() -> Principal {
let caller = ic_cdk::api::msg_caller();
if caller == Principal::anonymous() {
ic_cdk::trap("Anonymous principal not allowed. Please authenticate.");
}
caller
}
#[update]
fn init_owner() -> String {
// 防御性编程:在任何.await调用前捕获调用方信息。
let caller = require_auth();
OWNER.with(|owner| {
let mut cell = owner.borrow_mut();
let current = *cell.get();
if current == Principal::anonymous() {
cell.set(caller);
format!("Owner set to {}", caller)
} else {
"Owner already initialized".to_string()
}
})
}
#[update]
fn admin_action() -> String {
let caller = require_auth();
OWNER.with(|owner| {
let cell = owner.borrow();
let current = *cell.get();
if current == Principal::anonymous() {
ic_cdk::trap("Owner not set. Call init_owner first.");
} else if current == caller {
"Admin action performed".to_string()
} else {
ic_cdk::trap("Only the owner can call this function.");
}
})
}
#[query]
fn who_am_i() -> String {
let caller = ic_cdk::api::msg_caller();
if caller == Principal::anonymous() {
"You are not authenticated (anonymous)".to_string()
} else {
format!("Your principal: {}", caller)
}
}
// 对于异步函数,防御性地在await前捕获调用方信息:
#[update]
async fn protected_async_action() -> String {
let caller = require_auth(); // 在任何await前捕获
let _result = some_async_operation().await;
format!("Action completed by {}", caller)
}Rust防御性实践:在异步更新函数顶部添加。当前ic-cdk执行器会通过受保护任务在.await节点间保留调用方信息,但提前捕获该信息可防范未来执行器变更带来的问题。
let caller = ic_cdk::api::msg_caller();Deploy & Test
部署与测试
Local Deployment
本地部署
bash
undefinedbash
undefinedStart the local network
启动本地网络
icp network start -d
icp network start -d
Deploy II canister and your backend
部署II容器和你的后端
icp deploy internet_identity
icp deploy backend
icp deploy internet_identity
icp deploy backend
Verify II is running
验证II是否正常运行
icp canister status internet_identity
undefinedicp canister status internet_identity
undefinedMainnet Deployment
主网部署
bash
undefinedbash
undefinedII is already on mainnet -- only deploy your canisters
II已部署在主网——只需部署你的容器即可
icp deploy -e ic backend
undefinedicp deploy -e ic backend
undefinedVerify It Works
验证功能正常
bash
undefinedbash
undefined1. Check II canister is running
1. 检查II容器是否运行
icp canister status internet_identity
icp canister status internet_identity
Expected: Status: Running
预期结果:Status: Running
2. Test anonymous rejection from CLI
2. 通过CLI测试匿名调用被拒绝
icp canister call backend adminAction
icp canister call backend adminAction
Expected: Error containing "Anonymous principal not allowed"
预期结果:错误信息包含"Anonymous principal not allowed"
3. Test whoAmI as anonymous
3. 匿名状态下测试whoAmI
icp canister call backend whoAmI
icp canister call backend whoAmI
Expected: ("You are not authenticated (anonymous)")
预期结果:("You are not authenticated (anonymous)")
4. Test whoAmI as authenticated identity
4. 已认证状态下测试whoAmI
icp canister call backend whoAmI
icp canister call backend whoAmI
Expected: ("Your principal: <your-identity-principal>")
预期结果:("Your principal: <your-identity-principal>")
Note: icp CLI calls use the current identity, not anonymous,
注意:icp CLI默认使用当前身份调用,而非匿名身份,
unless you explicitly use --identity anonymous
除非你显式使用--identity anonymous参数
5. Test with explicit anonymous identity
5. 使用显式匿名身份测试
icp identity use anonymous
icp canister call backend adminAction
icp identity use anonymous
icp canister call backend adminAction
Expected: Error containing "Anonymous principal not allowed"
预期结果:错误信息包含"Anonymous principal not allowed"
icp identity use default # Switch back
icp identity use default # 切换回默认身份
6. Open II in browser for local dev
6. 本地开发时在浏览器中打开II
Visit: http://<internet_identity_canister_id>.localhost:8000
访问:http://<internet_identity_canister_id>.localhost:8000
undefinedundefinedYou should see the Internet Identity login page
你应该能看到Internet Identity登录页面
undefinedundefined