[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6WS1QFS0F0HRHB0F05WRGKS":3,"skill-same:01M6WS1QFS0F0HRHB0F05WRGKS":94},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":16,"sourceHtmlUrl":17,"categories":18,"keywords":27,"publishedAt":48,"linkOnly":49,"markdownContent":50,"markdownBody":51,"frontMatter":52,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":60,"repositoryHtmlUrl":61,"repositoryHomepageUrl":6,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M6WS1QFS0F0HRHB0F05WRGKS","internet-identity-attributes",null,"Trust a signed-in user's verified email, display name or organization SSO domain in a canister, rather than only their principal. Internet Identity signs an attribute bundle alongside the delegation, and the backend verifies the signer, the frontend origin and a nonce it issued itself before believing any of it. Use when a canister must know something about the caller beyond their principal, when gating access or an allowlist on an address the user provably controls, when showing a name the user never typed into your app, or when the backend rejects a bundle the frontend has just obtained. Covers the frontend request, scoping attributes to the account signed in with, and the identity-attributes library for Motoko and Rust. Assumes sign-in already works: load internet-identity first. Do NOT use for the sign-in flow itself, for an email address the user typed in (that is ordinary app input, which II has not checked), or for an agent or CLI acting as a user (agent-web-identity).","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",81,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity-attributes\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity-attributes\u002FSKILL.md",[19,24],{"categoryNo":20,"categoryName":21,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",{"categoryNo":25,"categoryName":26,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000010","后端与接口",[28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"internet identity attributes","identity","internet identity","attributes","internet","app","when","gating","request","display","which","frontend","organization","only","never","access","delegation","auth","rust","cli","2026-11-10 10:12:30",false,"---\nname: internet-identity-attributes\ndescription: \"Trust a signed-in user's verified email, display name or organization SSO domain in a canister, rather than only their principal. Internet Identity signs an attribute bundle alongside the delegation, and the backend verifies the signer, the frontend origin and a nonce it issued itself before believing any of it. Use when a canister must know something about the caller beyond their principal, when gating access or an allowlist on an address the user provably controls, when showing a name the user never typed into your app, or when the backend rejects a bundle the frontend has just obtained. Covers the frontend request, scoping attributes to the account signed in with, and the identity-attributes library for Motoko and Rust. Assumes sign-in already works: load internet-identity first. Do NOT use for the sign-in flow itself, for an email address the user typed in (that is ordinary app input, which II has not checked), or for an agent or CLI acting as a user (agent-web-identity).\"\nlicense: Apache-2.0\ncompatibility: \"moc >= 1.6.0, ic-cdk >= 0.20.1\"\nmetadata:\n  title: Internet Identity Attributes\n  category: Auth\n---\n\n# Internet Identity Attributes\n\n## Prerequisites\n\nThis skill assumes a working sign-in built with `@icp-sdk\u002Fauth`: load the `internet-identity` skill first. The attribute bundle travels alongside a delegation, so there is nothing here to request without one.\n\nThe `frontend_origins` value configured below must be the origin identities are derived for, the **primary origin** where the app has pinned one, not the origin the user happens to be visiting. A suite gets this wrong by default.\n\n- For the Motoko backend: `mo:identity-attributes` >= 0.4.0 (mops), the mixin that injects the two sign-in methods and verifies the attribute bundle. It pulls in `mo:core` >= 2.5.0 and requires `moc` >= 1.6.0 for `include`.\n- For the Rust backend: the `identity-attributes` crate and `ic-cdk` >= 0.20.1.\n\n## Identity attributes\n\nWhen the backend needs more than the principal (e.g. a verified email), II returns a signed attribute bundle alongside the delegation. The backend exposes two methods: `_internet_identity_sign_in_start` issues a nonce, `_internet_identity_sign_in_finish` verifies the bundle. The `identity-attributes` library adds both: the `mo:identity-attributes` mixin in Motoko, and the `#[identity_attributes]` attribute in Rust (see \"Backend: Rust\" below). Both return the same Candid types, so the frontend is the same against either.\n\n## Keys\n\n`requestAttributes({ keys, nonce })` requires both; there is no default key set.\n\n| Key | Meaning | Use for |\n|---|---|---|\n| `name` | Display name from the user's II-linked account | Personalisation |\n| `email` | Raw address from the II-linked account; **II does not check it** | Contact info, mailing lists |\n| `verified_email` | Present only when II established the user controls the address (an OpenID provider marked it verified, or the user verified it with II) | Access gating; the only email to authorise on |\n\nRequest both `email` and `verified_email` for fallback: when verified, both carry the same value; otherwise only `email` arrives.\n\n`scopedKeys` scopes keys to the account the user signs in with, so they are shared in the same step with no extra prompt:\n\n| Call | Keys | Default `keys` |\n|---|---|---|\n| `scopedKeys({ openIdProvider: 'google' })` | `openid:https:\u002F\u002Faccounts.google.com:\u003Ckey>` | `name`, `email`, `verified_email` |\n| `scopedKeys({ openIdProvider: 'apple' })` | `openid:https:\u002F\u002Fappleid.apple.com:\u003Ckey>` | same |\n| `scopedKeys({ openIdProvider: 'microsoft' })` | `openid:https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0:\u003Ckey>` (`{tid}` literal) | same |\n| `scopedKeys({ ssoDomain: 'acme.com' })` | `sso:acme.com:\u003Ckey>` | `name`, `email` (no `verified_email`) |\n\nPass `keys` to narrow, e.g. `scopedKeys({ openIdProvider: 'google', keys: ['name', 'verified_email'] })`.\n\n## Frontend\n\n```javascript\nimport { AuthClient } from \"@icp-sdk\u002Fauth\u002Fclient\";\nimport { AttributesIdentity } from \"@icp-sdk\u002Fcore\u002Fidentity\";\nimport { HttpAgent, Actor } from \"@icp-sdk\u002Fcore\u002Fagent\";\nimport { safeGetCanisterEnv } from \"@icp-sdk\u002Fcore\u002Fagent\u002Fcanister-env\";\nimport { Principal } from \"@icp-sdk\u002Fcore\u002Fprincipal\";\n\nconst II_PRINCIPAL = \"rdmx6-jaaaa-aaaaa-aaadq-cai\";\nconst rootKey = safeGetCanisterEnv()?.IC_ROOT_KEY;\n\nasync function signInWithAttributes(authClient, canisterId, idl) {\n  \u002F\u002F Anonymous handle, used only to ask for the nonce.\n  const anonymousActor = Actor.createActor(idl, { agent: await HttpAgent.create({ rootKey }), canisterId });\n\n  \u002F\u002F In parallel, one interaction for the user. `nonce` is a function the client\n  \u002F\u002F calls when it needs the value, so the request is in flight while II opens.\n  const signInPromise = authClient.signIn();\n  const attributesPromise = authClient.requestAttributes({\n    keys: [\"name\", \"verified_email\"],\n    nonce: () => anonymousActor._internet_identity_sign_in_start(),\n  });\n\n  const identity = await signInPromise;\n  const attributes = await attributesPromise;\n\n  \u002F\u002F The bundle travels as sender_info on each call made with this identity.\n  const verifiedAgent = await HttpAgent.create({\n    identity: new AttributesIdentity({\n      inner: identity,\n      attributes,\n      signer: { canisterId: Principal.fromText(II_PRINCIPAL) },\n    }),\n    rootKey,\n  });\n  const verifiedActor = Actor.createActor(idl, { agent: verifiedAgent, canisterId });\n\n  const result = await verifiedActor._internet_identity_sign_in_finish();\n  if (\"err\" in result) {\n    throw new Error(`Attribute verification failed: ${JSON.stringify(result.err)}`);\n  }\n  return identity;\n}\n```\n\nFor one-click sign-in, build the client with `openIdProvider` or `ssoDomain`, add `scopedKeys` to the `@icp-sdk\u002Fauth\u002Fclient` import, and request `scopedKeys({ openIdProvider: \"google\", keys: [\"name\", \"verified_email\"] })` or `scopedKeys({ ssoDomain: \"acme.com\" })` instead; the rest is unchanged.\n\nEvery bundle carries three implicit fields the backend MUST verify: `implicit:nonce` (one it issued and has not consumed), `implicit:origin` (a trusted frontend origin), `implicit:issued_at_timestamp_ns` (fresh, typically within five minutes).\n\n## Backend: Motoko\n\n```toml\n[dependencies]\nidentity-attributes = \"0.4.1\"\ncore                = \"2.5.0\"\n\n[toolchain]\nmoc = \"1.6.0\"\n```\n\n`include IdentityAttributes({ onVerified })` injects both methods and runs `onVerified` only on a bundle that passes the signer, origin, nonce, and freshness checks. It resolves the bundle to `{ name : ?Text; email : ?Text; sso : ?Text }`:\n\n- OpenID and unscoped sources: `email` comes from `verified_email` (or `openid:\u003Cprovider>:verified_email`), which is why the frontend requests `verified_email`.\n- SSO sources: `name` and `email` come from `sso:\u003Cdomain>:name` \u002F `sso:\u003Cdomain>:email`, accepted only for a domain in `trusted_sso_domains`, and `sso` is that domain. Otherwise `sso` is `null`.\n\n```motoko\nimport IdentityAttributes \"mo:identity-attributes\";\nimport Map \"mo:core\u002FMap\";\nimport Principal \"mo:core\u002FPrincipal\";\n\npersistent actor {\n  type Profile = { name : ?Text; email : ?Text; sso : ?Text };\n\n  let profiles = Map.empty\u003CPrincipal, Profile>();\n\n  include IdentityAttributes({\n    onVerified = func(caller, attrs) {\n      profiles.add(caller, attrs);\n    };\n  });\n\n  public query func getProfile(userId : Principal) : async ?Profile {\n    profiles.get(userId)\n  };\n};\n```\n\n```yaml\ncanisters:\n  - name: backend\n    settings:\n      environment_variables:\n        # Required. A local II (`ii: true`) has the same principal.\n        trusted_attribute_signers: \"rdmx6-jaaaa-aaaaa-aaadq-cai\"\n        # Required, comma-separated. The origin identities are DERIVED for.\n        frontend_origins: \"https:\u002F\u002Fyour-app.icp.net\"\n        # Optional, comma-separated; omit to reject all sso:* keys.\n        trusted_sso_domains: \"your-org.com\"\n```\n\nUnset `trusted_attribute_signers` rejects every bundle as untrusted; unset `frontend_origins` makes `_internet_identity_sign_in_finish` return `#err(#FrontendOriginsNotConfigured)`. The error variants (`#NoAttributes`, `#MalformedCandid`, `#FrontendOriginMismatch`, `#Stale`, `#UnknownNonce`, `#AmbiguousAttribute`, `#UntrustedSsoSource`, `#MixedSsoSources`) tell the frontend whether to retry with a fresh nonce or surface a bug.\n\n## Backend: Rust\n\nThe `identity-attributes` crate is the Rust side of the `mo:identity-attributes` Motoko package: the same checks, the same environment variables, and the same Candid types, so one frontend works against either backend.\n\n```toml\n[dependencies]\ncandid = \"0.10\"\nic-cdk = \"0.20.1\"\nidentity-attributes = \"0.1\"\n```\n\n```yaml\ncanisters:\n  - name: backend\n    settings:\n      environment_variables:\n        trusted_attribute_signers: \"rdmx6-jaaaa-aaaaa-aaadq-cai\"  # required\n        frontend_origins: \"https:\u002F\u002Fyour-app.icp.net\"              # required, comma-separated\n        trusted_sso_domains: \"acme.com\"                           # optional, comma-separated; omit to reject all sso: keys\n```\n\n`#[identity_attributes]` marks the function that receives the caller and their verified attributes, called only for a bundle that passes every check, and adds `_internet_identity_sign_in_start` and `_internet_identity_sign_in_finish`. The function cannot be `async` or generic:\n\n```rust\nuse candid::Principal;\nuse ic_cdk::query;\nuse identity_attributes::{identity_attributes, IdentityAttributes};\nuse std::cell::RefCell;\nuse std::collections::BTreeMap;\n\nthread_local! {\n    \u002F\u002F On the heap to keep the example short: an upgrade clears it.\n    static PROFILES: RefCell\u003CBTreeMap\u003CPrincipal, IdentityAttributes>> =\n        const { RefCell::new(BTreeMap::new()) };\n}\n\n#[identity_attributes]\nfn on_verified(caller: Principal, attributes: IdentityAttributes) {\n    PROFILES.with_borrow_mut(|profiles| profiles.insert(caller, attributes));\n}\n\n#[query]\nfn get_profile(user_id: Principal) -> Option\u003CIdentityAttributes> {\n    PROFILES.with_borrow(|profiles| profiles.get(&user_id).cloned())\n}\n\nic_cdk::export_candid!();\n```\n\n`IdentityAttributes` is `{ name: Option\u003CString>, email: Option\u003CString>, sso: Option\u003CString> }`:\n\n- `email` comes from `verified_email` (or `openid:\u003Cprovider>:verified_email`), and for an SSO sign-in from `sso:\u003Cdomain>:email`. The unverified `email` key of other sources is never read.\n- `sso` is the organization's domain when the values came from `sso:` keys, and `None` otherwise.\n\nWhat the finish method checks, in order, and the `err` it returns:\n\n| Check | Error |\n|---|---|\n| A bundle is attached | `NoAttributes` |\n| It decodes to an ICRC-3 `Value::Map` | `MalformedCandid` |\n| `frontend_origins` is set | `FrontendOriginsNotConfigured` |\n| `implicit:origin` is one of `frontend_origins` | `FrontendOriginMismatch { expected, got }` |\n| `implicit:issued_at_timestamp_ns` is at most five minutes old | `Stale { ageNs }` |\n| `implicit:nonce` was issued here and not consumed | `UnknownNonce` |\n| A required implicit field is present | `MissingField` |\n| Every `sso:\u003Cdomain>:*` key's domain is trusted | `UntrustedSsoSource { domain }` |\n| Name and email come from `sso:` keys or from others, never both | `MixedSsoSources { ssoKeys, otherKeys }` |\n| Each field comes from one key, and `sso:` keys from one domain | `AmbiguousAttribute { field, sources }` |\n\nThe result is Candid `variant { ok; err : Error }`, so the frontend checks `\"err\" in result`. A call carrying a bundle traps when `trusted_attribute_signers` is unset or does not list the bundle's signer.\n\nA nonce does not survive an upgrade: a sign-in that was in flight across one fails with `UnknownNonce`, and the frontend's remedy is to start again from `_internet_identity_sign_in_start`. The same error answers a nonce older than five minutes, or one already spent. Treat `UnknownNonce` as \"retry with a fresh nonce\" rather than as a bug.\n\n`identity_attributes::sign_in_start()` and `identity_attributes::sign_in_finish(on_verified)` are the functions behind the two methods, for a canister that defines them itself.\n\n## Mistakes That Break Your Build\n\n1. **Generating the attribute nonce on the frontend.** The nonce passed to `requestAttributes` MUST come from the backend (`_internet_identity_sign_in_start`), or the canister cannot check that the bundle's `implicit:nonce` is one it issued, and replay protection is gone.\n2. **Reading attribute data without verifying the signer.** The IC verifies the signature, not who signed: any canister can produce a valid bundle. The trusted signer is `rdmx6-jaaaa-aaaaa-aaadq-cai`. Use the `identity-attributes` library with `trusted_attribute_signers` and `frontend_origins` configured. Hand-written Rust must check `msg_caller_info_signer()` before reading `msg_caller_info_data()`, or an attacker canister can forge `email = \"admin@you.com\"`.\n3. **Substituting `{tid}` in the Microsoft scoped-key prefix.** The `microsoft` provider URL is the literal `https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0`; keys look like `openid:https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0:email` exactly. Filling in a tenant ID misses every lookup.\n4. **Treating `email` as verified.** `email` is the raw address from the user's II-linked account; II does not check it, so treat it as user input. `verified_email` is present only when II established that the user controls the address: an OpenID provider marked it verified, or the user verified it with II. Gate access on `verified_email` only. There is **no `verified_email` under `sso:`**: an organization's SSO cannot produce one; its `sso:\u003Cdomain>:email` is asserted by that organization's own provider, so trust it only for domains you list (`trusted_sso_domains`). What an organization can and cannot assert about its own staff is the `internet-identity-sso` skill.\n","# Internet Identity Attributes\n\n## Prerequisites\n\nThis skill assumes a working sign-in built with `@icp-sdk\u002Fauth`: load the `internet-identity` skill first. The attribute bundle travels alongside a delegation, so there is nothing here to request without one.\n\nThe `frontend_origins` value configured below must be the origin identities are derived for, the **primary origin** where the app has pinned one, not the origin the user happens to be visiting. A suite gets this wrong by default.\n\n- For the Motoko backend: `mo:identity-attributes` >= 0.4.0 (mops), the mixin that injects the two sign-in methods and verifies the attribute bundle. It pulls in `mo:core` >= 2.5.0 and requires `moc` >= 1.6.0 for `include`.\n- For the Rust backend: the `identity-attributes` crate and `ic-cdk` >= 0.20.1.\n\n## Identity attributes\n\nWhen the backend needs more than the principal (e.g. a verified email), II returns a signed attribute bundle alongside the delegation. The backend exposes two methods: `_internet_identity_sign_in_start` issues a nonce, `_internet_identity_sign_in_finish` verifies the bundle. The `identity-attributes` library adds both: the `mo:identity-attributes` mixin in Motoko, and the `#[identity_attributes]` attribute in Rust (see \"Backend: Rust\" below). Both return the same Candid types, so the frontend is the same against either.\n\n## Keys\n\n`requestAttributes({ keys, nonce })` requires both; there is no default key set.\n\n| Key | Meaning | Use for |\n|---|---|---|\n| `name` | Display name from the user's II-linked account | Personalisation |\n| `email` | Raw address from the II-linked account; **II does not check it** | Contact info, mailing lists |\n| `verified_email` | Present only when II established the user controls the address (an OpenID provider marked it verified, or the user verified it with II) | Access gating; the only email to authorise on |\n\nRequest both `email` and `verified_email` for fallback: when verified, both carry the same value; otherwise only `email` arrives.\n\n`scopedKeys` scopes keys to the account the user signs in with, so they are shared in the same step with no extra prompt:\n\n| Call | Keys | Default `keys` |\n|---|---|---|\n| `scopedKeys({ openIdProvider: 'google' })` | `openid:https:\u002F\u002Faccounts.google.com:\u003Ckey>` | `name`, `email`, `verified_email` |\n| `scopedKeys({ openIdProvider: 'apple' })` | `openid:https:\u002F\u002Fappleid.apple.com:\u003Ckey>` | same |\n| `scopedKeys({ openIdProvider: 'microsoft' })` | `openid:https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0:\u003Ckey>` (`{tid}` literal) | same |\n| `scopedKeys({ ssoDomain: 'acme.com' })` | `sso:acme.com:\u003Ckey>` | `name`, `email` (no `verified_email`) |\n\nPass `keys` to narrow, e.g. `scopedKeys({ openIdProvider: 'google', keys: ['name', 'verified_email'] })`.\n\n## Frontend\n\n```javascript\nimport { AuthClient } from \"@icp-sdk\u002Fauth\u002Fclient\";\nimport { AttributesIdentity } from \"@icp-sdk\u002Fcore\u002Fidentity\";\nimport { HttpAgent, Actor } from \"@icp-sdk\u002Fcore\u002Fagent\";\nimport { safeGetCanisterEnv } from \"@icp-sdk\u002Fcore\u002Fagent\u002Fcanister-env\";\nimport { Principal } from \"@icp-sdk\u002Fcore\u002Fprincipal\";\n\nconst II_PRINCIPAL = \"rdmx6-jaaaa-aaaaa-aaadq-cai\";\nconst rootKey = safeGetCanisterEnv()?.IC_ROOT_KEY;\n\nasync function signInWithAttributes(authClient, canisterId, idl) {\n  \u002F\u002F Anonymous handle, used only to ask for the nonce.\n  const anonymousActor = Actor.createActor(idl, { agent: await HttpAgent.create({ rootKey }), canisterId });\n\n  \u002F\u002F In parallel, one interaction for the user. `nonce` is a function the client\n  \u002F\u002F calls when it needs the value, so the request is in flight while II opens.\n  const signInPromise = authClient.signIn();\n  const attributesPromise = authClient.requestAttributes({\n    keys: [\"name\", \"verified_email\"],\n    nonce: () => anonymousActor._internet_identity_sign_in_start(),\n  });\n\n  const identity = await signInPromise;\n  const attributes = await attributesPromise;\n\n  \u002F\u002F The bundle travels as sender_info on each call made with this identity.\n  const verifiedAgent = await HttpAgent.create({\n    identity: new AttributesIdentity({\n      inner: identity,\n      attributes,\n      signer: { canisterId: Principal.fromText(II_PRINCIPAL) },\n    }),\n    rootKey,\n  });\n  const verifiedActor = Actor.createActor(idl, { agent: verifiedAgent, canisterId });\n\n  const result = await verifiedActor._internet_identity_sign_in_finish();\n  if (\"err\" in result) {\n    throw new Error(`Attribute verification failed: ${JSON.stringify(result.err)}`);\n  }\n  return identity;\n}\n```\n\nFor one-click sign-in, build the client with `openIdProvider` or `ssoDomain`, add `scopedKeys` to the `@icp-sdk\u002Fauth\u002Fclient` import, and request `scopedKeys({ openIdProvider: \"google\", keys: [\"name\", \"verified_email\"] })` or `scopedKeys({ ssoDomain: \"acme.com\" })` instead; the rest is unchanged.\n\nEvery bundle carries three implicit fields the backend MUST verify: `implicit:nonce` (one it issued and has not consumed), `implicit:origin` (a trusted frontend origin), `implicit:issued_at_timestamp_ns` (fresh, typically within five minutes).\n\n## Backend: Motoko\n\n```toml\n[dependencies]\nidentity-attributes = \"0.4.1\"\ncore                = \"2.5.0\"\n\n[toolchain]\nmoc = \"1.6.0\"\n```\n\n`include IdentityAttributes({ onVerified })` injects both methods and runs `onVerified` only on a bundle that passes the signer, origin, nonce, and freshness checks. It resolves the bundle to `{ name : ?Text; email : ?Text; sso : ?Text }`:\n\n- OpenID and unscoped sources: `email` comes from `verified_email` (or `openid:\u003Cprovider>:verified_email`), which is why the frontend requests `verified_email`.\n- SSO sources: `name` and `email` come from `sso:\u003Cdomain>:name` \u002F `sso:\u003Cdomain>:email`, accepted only for a domain in `trusted_sso_domains`, and `sso` is that domain. Otherwise `sso` is `null`.\n\n```motoko\nimport IdentityAttributes \"mo:identity-attributes\";\nimport Map \"mo:core\u002FMap\";\nimport Principal \"mo:core\u002FPrincipal\";\n\npersistent actor {\n  type Profile = { name : ?Text; email : ?Text; sso : ?Text };\n\n  let profiles = Map.empty\u003CPrincipal, Profile>();\n\n  include IdentityAttributes({\n    onVerified = func(caller, attrs) {\n      profiles.add(caller, attrs);\n    };\n  });\n\n  public query func getProfile(userId : Principal) : async ?Profile {\n    profiles.get(userId)\n  };\n};\n```\n\n```yaml\ncanisters:\n  - name: backend\n    settings:\n      environment_variables:\n        # Required. A local II (`ii: true`) has the same principal.\n        trusted_attribute_signers: \"rdmx6-jaaaa-aaaaa-aaadq-cai\"\n        # Required, comma-separated. The origin identities are DERIVED for.\n        frontend_origins: \"https:\u002F\u002Fyour-app.icp.net\"\n        # Optional, comma-separated; omit to reject all sso:* keys.\n        trusted_sso_domains: \"your-org.com\"\n```\n\nUnset `trusted_attribute_signers` rejects every bundle as untrusted; unset `frontend_origins` makes `_internet_identity_sign_in_finish` return `#err(#FrontendOriginsNotConfigured)`. The error variants (`#NoAttributes`, `#MalformedCandid`, `#FrontendOriginMismatch`, `#Stale`, `#UnknownNonce`, `#AmbiguousAttribute`, `#UntrustedSsoSource`, `#MixedSsoSources`) tell the frontend whether to retry with a fresh nonce or surface a bug.\n\n## Backend: Rust\n\nThe `identity-attributes` crate is the Rust side of the `mo:identity-attributes` Motoko package: the same checks, the same environment variables, and the same Candid types, so one frontend works against either backend.\n\n```toml\n[dependencies]\ncandid = \"0.10\"\nic-cdk = \"0.20.1\"\nidentity-attributes = \"0.1\"\n```\n\n```yaml\ncanisters:\n  - name: backend\n    settings:\n      environment_variables:\n        trusted_attribute_signers: \"rdmx6-jaaaa-aaaaa-aaadq-cai\"  # required\n        frontend_origins: \"https:\u002F\u002Fyour-app.icp.net\"              # required, comma-separated\n        trusted_sso_domains: \"acme.com\"                           # optional, comma-separated; omit to reject all sso: keys\n```\n\n`#[identity_attributes]` marks the function that receives the caller and their verified attributes, called only for a bundle that passes every check, and adds `_internet_identity_sign_in_start` and `_internet_identity_sign_in_finish`. The function cannot be `async` or generic:\n\n```rust\nuse candid::Principal;\nuse ic_cdk::query;\nuse identity_attributes::{identity_attributes, IdentityAttributes};\nuse std::cell::RefCell;\nuse std::collections::BTreeMap;\n\nthread_local! {\n    \u002F\u002F On the heap to keep the example short: an upgrade clears it.\n    static PROFILES: RefCell\u003CBTreeMap\u003CPrincipal, IdentityAttributes>> =\n        const { RefCell::new(BTreeMap::new()) };\n}\n\n#[identity_attributes]\nfn on_verified(caller: Principal, attributes: IdentityAttributes) {\n    PROFILES.with_borrow_mut(|profiles| profiles.insert(caller, attributes));\n}\n\n#[query]\nfn get_profile(user_id: Principal) -> Option\u003CIdentityAttributes> {\n    PROFILES.with_borrow(|profiles| profiles.get(&user_id).cloned())\n}\n\nic_cdk::export_candid!();\n```\n\n`IdentityAttributes` is `{ name: Option\u003CString>, email: Option\u003CString>, sso: Option\u003CString> }`:\n\n- `email` comes from `verified_email` (or `openid:\u003Cprovider>:verified_email`), and for an SSO sign-in from `sso:\u003Cdomain>:email`. The unverified `email` key of other sources is never read.\n- `sso` is the organization's domain when the values came from `sso:` keys, and `None` otherwise.\n\nWhat the finish method checks, in order, and the `err` it returns:\n\n| Check | Error |\n|---|---|\n| A bundle is attached | `NoAttributes` |\n| It decodes to an ICRC-3 `Value::Map` | `MalformedCandid` |\n| `frontend_origins` is set | `FrontendOriginsNotConfigured` |\n| `implicit:origin` is one of `frontend_origins` | `FrontendOriginMismatch { expected, got }` |\n| `implicit:issued_at_timestamp_ns` is at most five minutes old | `Stale { ageNs }` |\n| `implicit:nonce` was issued here and not consumed | `UnknownNonce` |\n| A required implicit field is present | `MissingField` |\n| Every `sso:\u003Cdomain>:*` key's domain is trusted | `UntrustedSsoSource { domain }` |\n| Name and email come from `sso:` keys or from others, never both | `MixedSsoSources { ssoKeys, otherKeys }` |\n| Each field comes from one key, and `sso:` keys from one domain | `AmbiguousAttribute { field, sources }` |\n\nThe result is Candid `variant { ok; err : Error }`, so the frontend checks `\"err\" in result`. A call carrying a bundle traps when `trusted_attribute_signers` is unset or does not list the bundle's signer.\n\nA nonce does not survive an upgrade: a sign-in that was in flight across one fails with `UnknownNonce`, and the frontend's remedy is to start again from `_internet_identity_sign_in_start`. The same error answers a nonce older than five minutes, or one already spent. Treat `UnknownNonce` as \"retry with a fresh nonce\" rather than as a bug.\n\n`identity_attributes::sign_in_start()` and `identity_attributes::sign_in_finish(on_verified)` are the functions behind the two methods, for a canister that defines them itself.\n\n## Mistakes That Break Your Build\n\n1. **Generating the attribute nonce on the frontend.** The nonce passed to `requestAttributes` MUST come from the backend (`_internet_identity_sign_in_start`), or the canister cannot check that the bundle's `implicit:nonce` is one it issued, and replay protection is gone.\n2. **Reading attribute data without verifying the signer.** The IC verifies the signature, not who signed: any canister can produce a valid bundle. The trusted signer is `rdmx6-jaaaa-aaaaa-aaadq-cai`. Use the `identity-attributes` library with `trusted_attribute_signers` and `frontend_origins` configured. Hand-written Rust must check `msg_caller_info_signer()` before reading `msg_caller_info_data()`, or an attacker canister can forge `email = \"admin@you.com\"`.\n3. **Substituting `{tid}` in the Microsoft scoped-key prefix.** The `microsoft` provider URL is the literal `https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0`; keys look like `openid:https:\u002F\u002Flogin.microsoftonline.com\u002F{tid}\u002Fv2.0:email` exactly. Filling in a tenant ID misses every lookup.\n4. **Treating `email` as verified.** `email` is the raw address from the user's II-linked account; II does not check it, so treat it as user input. `verified_email` is present only when II established that the user controls the address: an OpenID provider marked it verified, or the user verified it with II. Gate access on `verified_email` only. There is **no `verified_email` under `sso:`**: an organization's SSO cannot produce one; its `sso:\u003Cdomain>:email` is asserted by that organization's own provider, so trust it only for domains you list (`trusted_sso_domains`). What an organization can and cannot assert about its own staff is the `internet-identity-sso` skill.\n",{"name":5,"license":15,"metadata":53,"description":7,"compatibility":56},{"title":54,"category":55},"Internet Identity Attributes","Auth","moc >= 1.6.0, ic-cdk >= 0.20.1","main","026f68530bf15badc68cd145ba3df910fc33246b","skills\u002Fchains\u002Fdfinity-official-internet-identity-attributes",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-14 02:27:51",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity-attributes",{"tool":72,"command":73},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity-attributes",{"tool":75,"command":76},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity-attributes","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity-attributes\u002FSKILL.md",true,[80,84,90],{"path":81,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":83,"sourceHtmlUrl":16},"LICENSE","许可证",10770,{"path":85,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":88,"sourceHtmlUrl":89},"SOURCE.md","REFERENCE","参考文档",346,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity-attributes\u002FSOURCE.md",{"path":91,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":92,"sourceHtmlUrl":93},"TRUST.auto.yaml",2191,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity-attributes\u002FTRUST.auto.yaml",[95,123,145,163,176,186],{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":100,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":48},"01M712C436EQYE43342Q4E7GG1","recipe-performance","Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[104,105,106,107,108,109,33,110,111,112,113,114,115,116,117,118,119,120,121,122],"performance","recipe performance","recipe","mm","sentry","harness","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":124,"name":125,"nameZh":6,"description":126,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":127,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":48},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[131,132,133,134,135,136,137,138,139,140,141,142,143,144],"mexc spot websocket","spot","websocket","api","workflows","public","listen","streams","depth","keys","for","api keys","maintenance","protobuf",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":149,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":48},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[153,132,154,155,156,157,158,142,141,140,159,160,161,136,162,135,134],"mexc spot rest","REST","admin","signing","Wallet","requests","actions","secure","data","orders",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":167,"sourceHtmlUrl":168,"categories":169,"keywords":170,"publishedAt":48},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[171,172,133,173,174,143,141,175,139,138,136,135],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":177,"name":178,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":179,"sourceHtmlUrl":180,"categories":181,"keywords":182,"publishedAt":48},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[183,184,185],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":187,"name":188,"nameZh":6,"description":189,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":190,"sourceHtmlUrl":191,"categories":192,"keywords":193,"publishedAt":48},"01M6H0Z28Q3R8J0S799GJQWXM3","verify-tests-cover-changed-lines","Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[194,195,196,197,198,199,200,201,202,203,204],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","use","patch","code","share"]