Skip to content

feat(internet-identity): update for @icp-sdk/auth v7 API and identity attributes#182

Open
sea-snake wants to merge 11 commits intomainfrom
update-internet-identity-authclient-v6
Open

feat(internet-identity): update for @icp-sdk/auth v7 API and identity attributes#182
sea-snake wants to merge 11 commits intomainfrom
update-internet-identity-authclient-v6

Conversation

@sea-snake
Copy link
Copy Markdown

@sea-snake sea-snake commented May 4, 2026

Summary

Brings the internet-identity skill in line with the latest @icp-sdk/auth (v6) and the new system APIs for verified identity attributes.

Frontend (@icp-sdk/auth v6):

  • AuthClient.create() becomes new AuthClient(options); identityProvider is now passed to the constructor.
  • Callback-based login({ onSuccess, onError }) becomes promise-based await signIn({ maxTimeToLive }) returning the Identity directly.
  • isAuthenticated() is now sync; getIdentity() is now async.
  • One-click OpenID sign-in: openIdProvider: 'google' | 'apple' | 'microsoft'.
  • New requestAttributes({ keys, nonce }) flow with AttributesIdentity for sending signed attribute bundles alongside canister calls.

Backend (new msg_caller_info_* system APIs):

  • Rust: ic_cdk::api::msg_caller_info_data() / msg_caller_info_signer() (ic-cdk >= 0.20.1).
  • Motoko: Prim.callerInfoData<system>() / Prim.callerInfoSigner<system>().
  • Always verify the signer matches rdmx6-jaaaa-aaaaa-aaadq-cai (Internet Identity) before trusting the bundle: the IC checks the signature, not who signed it.
  • Bundle is Candid-encoded as an ICRC-3 Value::Map with implicit fields (implicit:nonce, implicit:origin, implicit:issued_at_timestamp_ns) plus the requested attribute keys.

Pitfalls added:

  • Mistake 3 reworded for the promise-based sign-in (no more onSuccess/onError).
  • Mistake 5 updated for the new failure modes.
  • Mistake 8 (new): generating the attribute nonce on the frontend defeats replay protection.
  • Mistake 9 (new): reading attribute data without verifying the signer lets any canister forge attributes.

Code example rewritten end-to-end against the new API.

npm run validate passes (warnings only, all pre-existing missing-evaluations warnings unrelated to this change).

Companion PR: dfinity/developer-docs#189 updates the consumer page that this skill backs.

@sea-snake sea-snake requested review from a team and JoshDFN as code owners May 4, 2026 16:35
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

Skill Validation Report

Validating skill: /home/runner/work/icskills/icskills/skills/internet-identity

Structure

  • Pass: SKILL.md found

Frontmatter

  • Pass: name: "internet-identity" (valid)
  • Pass: description: (325 chars)
  • Pass: license: "Apache-2.0"
  • Pass: compatibility: (31 chars)
  • Pass: metadata: (2 entries)

Tokens

  • Warning: SKILL.md body is 6596 tokens (spec recommends < 5000)

Markdown

  • Pass: no unclosed code fences found

Tokens

File Tokens
SKILL.md body 6,596
Total 6,596

Content Analysis

Metric Value
Word count 3,488
Code block ratio 0.43
Imperative ratio 0.09
Information density 0.26
Instruction specificity 0.90
Sections 13
List items 33
Code blocks 7

Contamination Analysis

Metric Value
Contamination level medium
Contamination score 0.24
Primary language category javascript
Scope breadth 3
  • Warning: Language mismatch: config, systems (2 categories differ from primary)

Result: 1 warning

Project Checks


✓ Project checks passed for 1 skills (0 warnings)

Copy link
Copy Markdown
Member

@marc0olo marc0olo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontend API migration and the new identity-attributes section are solid and consistent with the merged developer-docs PR #193. A few things worth addressing before merging:

PR title mismatch
Title says "@icp-sdk/auth v6 API" but the compatibility field is bumped to >= 7.0.0, which matches what the developer-docs PR calls it. Minor, but worth correcting so the history is clear.

@icp-sdk/core version not bumped
Prerequisites updates @icp-sdk/auth to >= 7.0.0 but leaves @icp-sdk/core at >= 5.0.0. The new example imports AttributesIdentity from @icp-sdk/core/identity — if that export requires a newer core release, agents following this skill will hit a missing-module error. Worth confirming the minimum is still 5.0.0.

Missing maxTimeToLive in registerWithEmail
The base signIn() example sets an 8-hour expiry; the registerWithEmail example calls authClient.signIn() with no options. Agents copying that pattern will use whatever the default delegation lifetime is. Should be consistent, or the omission explicitly noted.

OpenID scopedKeys example is incomplete
The snippet shows how to start the sign-in + attribute request in parallel for OpenID but doesn't show awaiting the promises or consuming the results. Compared to the full registerWithEmail example above it, this leaves agents guessing how to finish the flow.

Nonce verification is "omitted" in both backend examples
Mistake #8 makes a strong case that the nonce check is security-critical, but both registerFinish examples have // (omitted) where the check should be. That teaches agents to skip it. Even a minimal if (nonce != expected_nonce) { trap } stub would help.

Evaluations
Mistakes #8 and #9 are exactly the adversarial, non-obvious pitfalls the eval suite is designed to catch — an agent without the skill would almost certainly get both wrong. Two new output evals would be appropriate:

  • Mistake #8: agent is asked to implement requestAttributes with a frontend-generated nonce → must flag/reject that approach
  • Mistake #9: agent is asked to read msg_caller_info_data in Rust or Prim.callerInfoData in Motoko → must verify the signer before reading the data

Also, eval #4 ("Authenticated actor creation") has the expected behavior "Gets the identity from authClient.getIdentity()" — now that getIdentity() is async it should read "Calls await authClient.getIdentity()". And eval #5's expected behaviors should reflect the updated failure modes from Mistake #5 (constructor-passed identityProvider, unhandled signIn() rejection).

- bump @icp-sdk/core prerequisite to >= 5.3.0 (AttributesIdentity was
  introduced in core v5.3.0)
- pass maxTimeToLive to signIn() in registerWithEmail for consistency
  with the other examples
- complete the OpenID scopedKeys example through AttributesIdentity
  wrapping and the protected-method call
- replace omitted nonce/timestamp checks with explicit lookup helpers
  and comparisons in both Motoko and Rust register_finish examples
- add evals for Mistake 8 (frontend-generated nonce) and Mistake 9
  (reading attribute data without verifying signer)
- update eval 4 expected behavior to reflect async getIdentity()
- update eval 5 (anonymous principal) for the v6+ failure modes
@sea-snake sea-snake changed the title feat(internet-identity): update for @icp-sdk/auth v6 API and identity attributes feat(internet-identity): update for @icp-sdk/auth v7 API and identity attributes May 5, 2026
@sea-snake
Copy link
Copy Markdown
Author

Feedback addressed in 95c5376:

  • @icp-sdk/core version: bumped prerequisite to >= 5.3.0 (AttributesIdentity was added in core v5.3.0).
  • Missing maxTimeToLive in registerWithEmail: now passes the same 8-hour expiry as the base example.
  • OpenID scopedKeys example: completed through awaiting both promises, wrapping with AttributesIdentity, and calling the protected method.
  • Nonce verification "omitted": both Motoko and Rust register_finish now do an explicit lookupBlob / lookup_blob of implicit:nonce, compare against a stored expected value, and trap on mismatch. Added a parallel timestamp freshness check (5-minute window) using implicit:issued_at_timestamp_ns. Helper functions for Blob/Nat lookups added in both languages.
  • Evaluations:
    • Updated eval 4 ("Authenticated actor creation") to reference await authClient.getIdentity() (now async).
    • Updated eval 5 to "Debugging anonymous principal after sign-in" with new failure modes (constructor identityProvider, unhandled signIn() rejection).
    • Added new adversarial eval for Mistake 8 (frontend-generated attribute nonce).
    • Added new adversarial eval for Mistake 9 (reading attribute data without verifying signer).
  • PR title: corrected from "v6 API" to "v7 API".

npm run validate still passes (warnings only, all pre-existing).

New eval results (with-skill vs baseline)

Both new adversarial evals show a strong skill-vs-baseline gap, exactly the pattern the eval suite is designed to catch.

Eval 6 — Adversarial: frontend-generated attribute nonce

  • WITH skill: 4/4 passed
  • WITHOUT skill: 0/4 passed
    • Baseline output endorsed crypto.getRandomValues as the nonce source and gave a complete implementation around it. Never mentioned replay protection, backend-issued nonces, or canister verification.

Eval 7 — Adversarial: reading attribute data without verifying signer

  • WITH skill: 4/4 passed
  • WITHOUT skill: 0/4 passed
    • Baseline output declined to write any code (asked clarifying questions). With the skill loaded, the response correctly verifies the signer against rdmx6-jaaaa-aaaaa-aaadq-cai before trusting the bundle, traps on mismatch, and explains the signer-vs-signature distinction.

…s verified_email

- list the unscoped attribute keys (name, email, verified_email) with
  guidance on when to use each: email for soft uses (mailing lists,
  contact), verified_email for access gating (admin allowlists)
- document scopedKeys() resolution including the literal Microsoft
  provider URL (the {tid} segment is part of the URL, not a tenant
  placeholder)
- add Mistake 10: substituting {tid} into the Microsoft URL silently
  breaks attribute lookups
- add Mistake 11: treating email as verified — only verified_email
  carries the source provider's verification signal
- new evals: Microsoft tid substitution, email-vs-verified_email for
  access gating
@sea-snake
Copy link
Copy Markdown
Author

Follow-up in 65a1dd7: agents had no way to know which attribute keys exist or which to pick. Added an "Available attribute keys" subsection that lists name, email, verified_email and explains when to use each (email for soft uses like contact / mailing lists, verified_email for access gating since only that key carries the source OpenID provider's verification signal). Documented scopedKeys() resolution per provider, including the literal Microsoft URL.

Two new pitfalls:

  • Mistake 10: substituting {tid} into the Microsoft scoped-key URL. The {tid} is part of the literal URL, not a placeholder.
  • Mistake 11: treating email as verified.
New eval results (with-skill vs baseline)

Eval 8 — Adversarial: substituting tid in the Microsoft scoped-key URL

  • WITH skill: 3/3 passed
  • WITHOUT skill: 0/3 passed
    • Baseline output explicitly instructed the user to replace {tid} with the actual tenant ID and validated JWTs against the tenant-resolved issuer URL. With the skill, the response correctly tells the user not to substitute and to look up the literal key.

Eval 9 — Adversarial: using email instead of verified_email for access gating

  • WITH skill: 3/4 passed
  • WITHOUT skill: 1/4 passed
    • Baseline never mentioned verified_email, claimed II does not expose email, and redirected to Principal-based gating or NFID. With the skill, the response requests verified_email, performs the implicit-field and signer checks, and gates on the verified value. The one with-skill miss was the judge being strict that the answer didn't also explicitly define what plain email IS — only that it's untrustworthy.

- table column renamed to "What it IS" and rows lead with the explicit
  definition (raw vs verified) so agents echo the distinction in their
  answers
- Mistake 11 split into two bullets that define each key independently
- eval 9 expected behaviour split: agents now have to articulate the
  email definition AND the verified_email definition separately

eval 9 now passes 5/5 with-skill (was 3/4) vs 1/5 baseline
@sea-snake
Copy link
Copy Markdown
Author

fc65fef tightens the email vs verified_email guidance so the previous one-of-four miss on eval 9 lands. The compound expected behavior was split into two simpler ones (one defining email, one defining verified_email), and the SKILL prose now leads each row of the attribute-keys table with what the key IS rather than what it isn't.

Updated eval 9 result

Eval 9 — Adversarial: using email instead of verified_email for access gating

  • WITH skill: 5/5 passed (was 3/4)
  • WITHOUT skill: 1/5 passed
    • Baseline never mentioned verified_email, deflected to principal-based auth, and skipped the bundle verification entirely. With the skill loaded, the response now correctly articulates both definitions, requests verified_email, performs the implicit-field and signer checks, and gates on the verified value.

Copy link
Copy Markdown
Member

@marc0olo marc0olo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on the latest commits — the original six concerns are all cleanly resolved and the eval additions are excellent. Three issues in the new code before this is merge-ready:

OpenID scopedKeys example: bare await at module scope
The entire code block sits at module level with multiple top-level await calls (backend.registerBegin(), signInPromise, attributesPromise, HttpAgent.create()). That triggers the exact Vite es2020 error that eval #6 already tests for. Needs the same async function wrapper as registerWithEmail.

Motoko: pendingNonces referenced as live code but declared only in comments

// var pendingNonces : PendingNonces = ...;  ← commented out
...
let ?expected = pendingNonces.remove(caller) ...  ← live code

Won't compile. The Rust version handles this cleanly with an explicit consume_pending_nonce stub + unimplemented!(). Motoko should do the same — a func consumePendingNonce(caller : Principal) : ?Blob { /* see Storing the nonce */ } stub.

Motoko: Nat64.toNat(Prim.time()) — use Time.now() from mo:core/Time instead
Nat64 is not imported, so Nat64.toNat won't compile. But more broadly: since mo:prim should only be used where no mo:core equivalent exists, prefer Time.now() from mo:core/Time for the timestamp. Time.now() returns Int (nanoseconds), and since Nat <: Int in Motoko the freshness comparison works directly without any conversion:

import Time "mo:core/Time";
...
let nowNs : Int = Time.now();
if (nowNs > issuedAt + 300_000_000_000) Runtime.trap("Bundle too old");

Prim is still required — callerInfoData<system> and callerInfoSigner<system> have no mo:core equivalent yet. Worth adding a short comment to that effect so future readers don't try to remove the import:

import Prim "mo:prim"; // callerInfoData / callerInfoSigner not yet in mo:core

@marc0olo
Copy link
Copy Markdown
Member

marc0olo commented May 5, 2026

One more finding that affects the Motoko backend example directly:

mo:core/CallerAttributes already exists (motoko-core v2.5.0)

caffeinelabs/motoko-core#491 added a CallerAttributes module that wraps both callerInfoSigner and callerInfoData into a single getAttributes<system>() : ?Blob call. The entire manual Prim dance in the current Motoko example can be replaced with:

import CallerAttributes "mo:core/CallerAttributes";
// ...
let ?data = CallerAttributes.getAttributes<system>() else Runtime.trap("no trusted attributes");

The signer check is baked in — getAttributes returns null if there are no attributes, and traps if the signer isn't in the trusted_attribute_signers canister environment variable. That means the trusted II principal (rdmx6-jaaaa-aaaaa-aaadq-cai) moves from a hardcoded constant in code to a deploy-time config. The skill should document how to set it, e.g. in icp.yaml:

canisters:
  backend:
    # ...
    env:
      trusted_attribute_signers: "rdmx6-jaaaa-aaaaa-aaadq-cai"

This resolves the Prim import concern for Motoko entirely — no mo:prim needed at all on the Motoko path. The Rust path (msg_caller_info_signer / msg_caller_info_data via ic-cdk) is unaffected; there is no equivalent CDK wrapper yet.

Also note this bumps the minimum mo:core version to >= 2.5.0 — worth reflecting in the Prerequisites or the Backend section.

@sea-snake
Copy link
Copy Markdown
Author

sea-snake commented May 6, 2026

Feedback addressed in 6a8a4cc:

Motoko: switch to mo:core/CallerAttributes. The manual Prim.callerInfoSigner / Prim.callerInfoData dance is gone on the Motoko path. The example now imports mo:core/CallerAttributes and reads the bundle with CallerAttributes.getAttributes<system>() : ?Blob. The trusted-signer check is baked into the wrapper via trusted_attribute_signers, so the II principal moves out of code and into deploy config.

icp.yaml config documented. Added a "Configuring trusted_attribute_signers" subsection with the right schema (settings.environment_variables — the actual icp-cli key — verified against dfinity/icp-cli docs/reference/canister-settings.md):

canisters:
  - name: backend
    settings:
      environment_variables:
        trusted_attribute_signers: "rdmx6-jaaaa-aaaaa-aaadq-cai"

With a note that an unset env var traps getAttributes, which is the right behavior.

Prerequisites bumped to mo:core >= 2.5.0 (where CallerAttributes was added).

mo:prim dropped on Motoko. No more import Prim "mo:prim" in the Motoko example. The Rust path is unchanged: there's no ic-cdk wrapper yet, so msg_caller_info_signer is still checked manually.

Mistake 9 split per language to make the asymmetry explicit:

  • Motoko: prefer the wrapper, configure the env var, the trap is automatic.
  • Rust: explicit msg_caller_info_signer() check before reading data.

Companion fixes folded in while the Motoko example was being rewritten:

  • Nat64.toNat(Prim.time())Time.now() from mo:core/Time (the earlier review's missing-import call). Time.now() : Int and Nat <: Int, so the freshness comparison works without a conversion.
  • pendingNonces is no longer referenced as live code with no declaration; it's a consumePendingNonce stub mirroring the Rust pattern.
  • The OpenID scopedKeys example is wrapped in an async function registerWithGoogle(...) so it doesn't trip the same Vite es2020 top-level-await failure mode that eval 6 already covers.

Eval 9 updated so the expected behavior accepts either the explicit Rust signer check or the Motoko env-var-based check (otherwise a correct Motoko answer using CallerAttributes would fail the eval for not showing a manual msg_caller_info_signer comparison).

Companion PR for developer-docs with the same Motoko swap: dfinity/developer-docs#207

npm run validate still passes (warnings only, all pre-existing patterns).

marc0olo pushed a commit to dfinity/developer-docs that referenced this pull request May 6, 2026
…kend example (#207)

## Summary

Companion to dfinity/icskills#182. Updates the Motoko backend example on
the Internet Identity guide to use `mo:core/CallerAttributes`
(motoko-core v2.5.0+) instead of the manual `Prim.callerInfoSigner` /
`Prim.callerInfoData` dance.

- `CallerAttributes.getAttributes<system>() : ?Blob` returns the bundle
and traps when the signer isn't listed in the canister's
`trusted_attribute_signers` env var. The hardcoded II principal moves
out of code and into deploy config.
- Drops the `mo:prim` import on the Motoko path (the wrapper handles
primitives + signer comparison).
- Adds an `icp.yaml` `settings.environment_variables` snippet for
declaring `trusted_attribute_signers`.
- Splits the per-language intro and the 'common mistakes' bullet so the
Rust path (still requires explicit `msg_caller_info_signer()` check, no
CDK wrapper yet) stays distinct from Motoko.

`npm run build` passes; no new agent-docs warnings on the affected file.

## Sync recommendation

informed by caffeinelabs/motoko-core (`src/CallerAttributes.mo`);
dfinity/icp-cli
(`docs/reference/canister-settings.md#environment_variables`);
dfinity/icskills (`skills/internet-identity/SKILL.md` companion PR #182)
Replaces the manual Prim.callerInfoSigner / Prim.callerInfoData dance
with CallerAttributes.getAttributes<system>() from mo:core (>= 2.5.0).
The wrapper bakes in the trusted-signer check via the canister's
trusted_attribute_signers env var, so the example no longer hardcodes
the II principal in code: it moves to icp.yaml as deploy-time config.

Notable changes:

- Motoko example now imports mo:core/CallerAttributes (no more mo:prim)
  and reads time via mo:core/Time (Time.now() : Int) instead of the
  broken Nat64.toNat(Prim.time()) which had no Nat64 import.
- consumePendingNonce stub mirrors the Rust register_finish pattern so
  the example compiles standalone.
- New "Configuring trusted_attribute_signers" subsection shows the
  icp.yaml settings.environment_variables snippet.
- Mistake #9 split per language: Motoko points at the env-var-based
  check, Rust still requires explicit msg_caller_info_signer.
- Prerequisites bumps mo:core minimum to >= 2.5.0.
- OpenID scopedKeys example wrapped in an async function to avoid
  bare top-level await at module scope (fixes the same Vite es2020
  failure mode eval #6 already covers).
- Eval #9 expected behavior accepts either the explicit Rust signer
  check or the Motoko env-var check.

Rust path is unchanged: there is no ic-cdk wrapper yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants