For AI agents: Documentation index at /llms.txt

Skip to content

Identity attributes

When your backend needs more than the user’s principal, for example a verified email address, Internet Identity can return identity attributes: a bundle of values signed by Internet Identity, which your frontend attaches to a call and your backend verifies. This page covers which attributes exist, how to request them, and how to verify them.

Available attributes

KeyWhat it isUse it for
nameThe user’s display name from their linked account.Personalization.
emailThe email address from their linked account. Internet Identity does not check it, so treat it as user-supplied input.Contact details, mailing lists.
verified_emailAn email address Internet Identity knows the user has access to: one that an OpenID provider such as Google marked as verified, or one that the user linked and verified with Internet Identity.Anything that gates access on the email, such as an admin allowlist.

requestAttributes has no default key set: pass the keys you need.

Scoped keys

Attributes can also be scoped to one source, so the value comes from that account and the user grants it in the same step they sign in with:

  • openid:<provider-url>:<key> for Google, Apple, or Microsoft, with name, email, and verified_email.
  • sso:<domain>:<key> for an organization’s SSO, with name and email. An organization’s SSO is run by that organization, so it never produces a verified_email.

scopedKeys from @icp-sdk/auth/client builds them:

import { scopedKeys } from "@icp-sdk/auth/client";
scopedKeys({ openIdProvider: "google", keys: ["name", "verified_email"] });
// ["openid:https://accounts.google.com:name", "openid:https://accounts.google.com:verified_email"]
scopedKeys({ ssoDomain: "acme.com" });
// ["sso:acme.com:name", "sso:acme.com:email"]

The Microsoft provider URL is the literal https://login.microsoftonline.com/{tid}/v2.0: {tid} is part of the key, not a placeholder for a tenant ID. Scoped keys pair with one-click sign-in, which sends the user to that same provider.

Request attributes with the sign-in

The backend starts the flow by issuing a single-use nonce, and verifies at the end that the bundle carries it. A nonce made by the frontend would let a captured bundle be replayed, so it must come from the canister.

The backend exposes two methods: _internet_identity_sign_in_start returns a nonce, and _internet_identity_sign_in_finish verifies the bundle the call carries. Sign-in and the attribute request run in parallel, so the user sees a single Internet Identity interaction:

import { AuthClient } from "@icp-sdk/auth/client";
import { AttributesIdentity } from "@icp-sdk/core/identity";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
import { Principal } from "@icp-sdk/core/principal";
const II_CANISTER_ID = "rdmx6-jaaaa-aaaaa-aaadq-cai";
const rootKey = safeGetCanisterEnv()?.IC_ROOT_KEY;
async function signInWithAttributes(authClient, canisterId, idlFactory) {
// Anonymous actor, used only to fetch the nonce.
const anonymous = Actor.createActor(idlFactory, {
agent: await HttpAgent.create({ rootKey }),
canisterId,
});
const signInPromise = authClient.signIn();
const attributesPromise = authClient.requestAttributes({
keys: ["name", "verified_email"],
// A function: the client calls it when it needs the nonce.
nonce: () => anonymous._internet_identity_sign_in_start(),
});
const identity = await signInPromise;
const attributes = await attributesPromise;
// The bundle travels with every call this agent makes.
const agent = await HttpAgent.create({
identity: new AttributesIdentity({
inner: identity,
attributes,
signer: { canisterId: Principal.fromText(II_CANISTER_ID) },
}),
rootKey,
});
const backend = Actor.createActor(idlFactory, { agent, canisterId });
const result = await backend._internet_identity_sign_in_finish();
if ("err" in result) {
throw new Error(`Attribute verification failed: ${JSON.stringify(result.err)}`);
}
return identity;
}

To request attributes later, for example when a user links an email to an existing account, expose another pair of methods and run the same steps: a fresh nonce, requestAttributes, and verification.

Verify attributes in the backend

The bundle is an ICRC-3 value map with the keys you requested plus three implicit fields. Before reading any attribute, the backend checks:

  1. The signer is Internet Identity, rdmx6-jaaaa-aaaaa-aaadq-cai. The network checks that the bundle is signed, not who signed it, and any canister can sign a bundle.
  2. implicit:origin is a frontend origin you trust, so another app cannot forward its users’ bundles to your backend.
  3. implicit:issued_at_timestamp_ns is recent; a few minutes is typical.
  4. implicit:nonce is one this canister issued and has not consumed yet; consume it.

The identity-attributes library, for Motoko and Rust, adds both methods and runs your function only for a bundle that passes every check:

Add it to mops.toml:

[dependencies]
identity-attributes = "0.4.1"
core = "2.5.0"
[toolchain]
moc = "1.6.0"
import IdentityAttributes "mo:identity-attributes";
import Map "mo:core/Map";
import Principal "mo:core/Principal";
persistent actor {
type Profile = { name : ?Text; email : ?Text; sso : ?Text };
let profiles = Map.empty<Principal, Profile>();
include IdentityAttributes({
onVerified = func(caller, attrs) {
profiles.add(caller, attrs);
};
});
public query func getProfile(userId : Principal) : async ?Profile {
profiles.get(userId)
};
};

Your function receives { name, email, sso }:

  • email comes from verified_email (or openid:<provider>:verified_email), and for an SSO sign-in from sso:<domain>:email. The library never reads the unverified email key of other sources, which is why the frontend requests verified_email.
  • sso is the organization’s domain when the values came from sso: keys, and empty otherwise.

Configure the library through the canister’s environment variables in icp.yaml:

canisters:
- name: backend
settings:
environment_variables:
trusted_attribute_signers: "rdmx6-jaaaa-aaaaa-aaadq-cai" # required
frontend_origins: "https://your-app.icp.net" # required, comma-separated
trusted_sso_domains: "acme.com" # optional, comma-separated; omit to reject all sso: keys

An unconfigured canister trusts nothing: without trusted_attribute_signers every bundle is rejected, and without frontend_origins the finish method returns err with FrontendOriginsNotConfigured. A bundle with an sso: key from a domain outside trusted_sso_domains, or one mixing sso: keys with keys of another source, is rejected as well. Both libraries return the same Candid types, so the same frontend works against either.

Common mistakes

  • Generating the nonce in the frontend. The canister cannot tell such a nonce from a replayed one. Always fetch it from _internet_identity_sign_in_start.
  • Reading the bundle without checking the signer. Any canister can sign a bundle with email = "admin@your-app.com"; only Internet Identity’s signature counts.
  • Gating access on email. It is not checked by Internet Identity. Use verified_email, or sso:<domain>:email from a domain you trust.
  • Substituting {tid} in the Microsoft key. It is literal; a key with a tenant ID in it matches nothing.

Next steps