Loading...
Loading...
Integrate Internet Identity authentication. Covers passkey and OpenID login flows, delegation handling, and principal-per-app isolation. Use when adding login, sign-in, auth, passkeys, or Internet Identity to a frontend or canister. Do NOT use for wallet integration or ICRC signer flows — use wallet-integration instead.
npx skill4agent add dfinity/icskills internet-identity@icp-sdk/auth@icp-sdk/coremopscore = "2.0.0"ic-cdk >= 0.19| Canister | ID | URL | Purpose |
|---|---|---|---|
| Internet Identity | | | Stores and manages user keys, serves the II web app over HTTPS |
http://<local-ii-canister-id>.localhost:8000https://id.aiicp deploy internet_identityic_envsafeGetCanisterEnv@icp-sdk/core/agent/canister-envauthClient.login()onSuccessonErrormsg_caller().await.awaitlet caller = ic_cdk::api::msg_caller();AuthClientIdentityshared(msg) { msg.caller }ic_cdk::api::msg_caller()agent.fetchRootKey()thread_local!thread_local! { RefCell<T> }StableCellic-stable-structuresiifrontendicp newstatic-website# 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 `npm ci` to lock your dependencies
- npm install
- npm run build
dir: dist
networks:
- name: local
mode: managed
ii: trueimport { 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
}npm install -g ic-mopsimport 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;
};
};# 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"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)
}let caller = ic_cdk::api::msg_caller();.await# Start the local network
icp network start -d
# Deploy II canister and your backend
icp deploy internet_identity
icp deploy backend
# Verify II is running
icp canister status internet_identity# II is already on mainnet -- only deploy your canisters
icp deploy -e ic backend# 1. Check II canister is running
icp canister status internet_identity
# Expected: Status: Running
# 2. Test anonymous rejection from CLI
icp canister call backend adminAction
# Expected: Error containing "Anonymous principal not allowed"
# 3. Test whoAmI as anonymous
icp canister call backend whoAmI
# Expected: ("You are not authenticated (anonymous)")
# 4. Test whoAmI as authenticated identity
icp canister call backend whoAmI
# Expected: ("Your principal: <your-identity-principal>")
# Note: icp CLI calls use the current identity, not anonymous,
# unless you explicitly use --identity anonymous
# 5. Test with explicit anonymous identity
icp identity use anonymous
icp canister call backend adminAction
# Expected: Error containing "Anonymous principal not allowed"
icp identity use default # Switch back
# 6. Open II in browser for local dev
# Visit: http://<internet_identity_canister_id>.localhost:8000undefined