internet-identity

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Internet 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:
    mops
    package manager,
    core = "2.0.0"
    in mops.toml
  • For Rust:
    ic-cdk >= 0.19
  • 前端:
    @icp-sdk/auth
    @icp-sdk/core
  • Motoko开发:
    mops
    包管理器,mops.toml中配置
    core = "2.0.0"
  • Rust开发:
    ic-cdk >= 0.19

Canister IDs

容器ID

CanisterIDURLPurpose
Internet Identity
rdmx6-jaaaa-aaaaa-aaadq-cai
https://id.ai
Stores and manages user keys, serves the II web app over HTTPS
容器IDURL用途
Internet Identity
rdmx6-jaaaa-aaaaa-aaadq-cai
https://id.ai
存储并管理用户密钥,通过HTTPS提供II网页应用服务

Mistakes That Break Your Build

导致构建失败的常见错误

  1. Using the wrong II URL for the environment. Local development must point to
    http://<local-ii-canister-id>.localhost:8000
    (this canister ID may be different from mainnet). Mainnet must use
    https://id.ai
    . Hardcoding one breaks the other. The local II canister ID is assigned dynamically when you run
    icp deploy internet_identity
    -- read it from the
    ic_env
    cookie using
    safeGetCanisterEnv
    from
    @icp-sdk/core/agent/canister-env
    (see the icp-cli skill for details on canister environment variables).
  2. 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.
  3. Not handling auth callbacks. The
    authClient.login()
    call requires
    onSuccess
    and
    onError
    callbacks. Without them, login failures are silently swallowed.
  4. Defensive practice: bind
    msg_caller()
    before
    .await
    in Rust.
    The current ic-cdk executor preserves the caller across
    .await
    points, but capturing it early guards against future executor changes. Always bind
    let caller = ic_cdk::api::msg_caller();
    at the top of async update functions.
  5. Passing principal as string to backend. The
    AuthClient
    gives you an
    Identity
    object. Backend canister methods receive the caller principal automatically via the IC protocol -- you do not pass it as a function argument. Use
    shared(msg) { msg.caller }
    in Motoko or
    ic_cdk::api::msg_caller()
    in Rust.
  6. Not calling
    agent.fetchRootKey()
    in local development.
    Without this, certificate verification fails on localhost. Never call it in production -- it's a security risk on mainnet.
  7. Storing auth state in
    thread_local!
    without stable storage (Rust)
    --
    thread_local! { RefCell<T> }
    is heap memory, wiped on every canister upgrade. Use
    StableCell
    from
    ic-stable-structures
    for any state that must persist across upgrades, especially ownership/auth data.
  1. 使用错误的II环境URL。本地开发必须指向
    http://<local-ii-canister-id>.localhost:8000
    (该容器ID可能与主网不同)。主网必须使用
    https://id.ai
    。硬编码其中一个会导致另一个环境无法正常工作。本地II容器ID是在运行
    icp deploy internet_identity
    时动态分配的——可使用
    @icp-sdk/core/agent/canister-env
    中的
    safeGetCanisterEnv
    ic_env
    cookie中读取(关于容器环境变量的详细信息请参考icp-cli技能文档)。
  2. 设置过长的委托有效期。委托的最大有效期为30天(2_592_000_000_000_000纳秒)。超过该值的设置会被自动截断,导致会话行为异常。普通应用建议设置为8小时,“记住我”场景最多设置为30天。
  3. 未处理认证回调。调用
    authClient.login()
    时必须传入
    onSuccess
    onError
    回调函数。如果不设置,登录失败的信息会被静默忽略。
  4. Rust防御性实践:在
    .await
    前绑定
    msg_caller()
    。当前ic-cdk执行器会在
    .await
    节点保留调用方信息,但提前捕获该信息可防范未来执行器变更带来的问题。在异步更新函数顶部务必添加
    let caller = ic_cdk::api::msg_caller();
  5. 将主体以字符串形式传递给后端
    AuthClient
    会返回一个
    Identity
    对象。后端容器方法会通过IC协议自动获取调用方主体——无需将其作为函数参数传递。Motoko中使用
    shared(msg) { msg.caller }
    ,Rust中使用
    ic_cdk::api::msg_caller()
    即可。
  6. 本地开发时未调用
    agent.fetchRootKey()
    。如果不调用该方法,本地环境下的证书验证会失败。注意:生产环境绝对不能调用该方法——这会给主网带来安全风险。
  7. 在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
ii
property to the local network to enable Internet Identity.
Here's an example icp.yaml configuration (assume that the
frontend
canister is generated using
icp new
using the
static-website
template):
yaml
undefined
本地开发时,只需在本地网络配置中添加
ii
属性即可启用Internet Identity。
以下是icp.yaml配置示例(假设
frontend
容器是使用
icp new
命令的
static-website
模板生成的):
yaml
undefined
canisters:
  • name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: # Install the dependencies # Eventually you might want to use
    npm ci
    to lock your dependencies - npm install - npm run build dir: dist
networks:
  • name: local mode: managed ii: true

<!-- Reviewed till here -->
canisters:
  • name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: # 安装依赖 # 最终你可能会使用
    npm ci
    来锁定依赖版本 - npm install - npm run build dir: dist
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-mops
motoko
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-mops
motoko
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
undefined
toml
undefined

Cargo.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
let caller = ic_cdk::api::msg_caller();
at the top of async update functions. The current ic-cdk executor preserves caller across
.await
points via protected tasks, but capturing it early guards against future executor changes.
[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防御性实践:在异步更新函数顶部添加
let caller = ic_cdk::api::msg_caller();
。当前ic-cdk执行器会通过受保护任务在.await节点间保留调用方信息,但提前捕获该信息可防范未来执行器变更带来的问题。

Deploy & Test

部署与测试

Local Deployment

本地部署

bash
undefined
bash
undefined

Start 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
undefined
icp canister status internet_identity
undefined

Mainnet Deployment

主网部署

bash
undefined
bash
undefined

II is already on mainnet -- only deploy your canisters

II已部署在主网——只需部署你的容器即可

icp deploy -e ic backend
undefined
icp deploy -e ic backend
undefined

Verify It Works

验证功能正常

bash
undefined
bash
undefined

1. 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

undefined
undefined

You should see the Internet Identity login page

你应该能看到Internet Identity登录页面

undefined
undefined