Icp identity link web --app <domain> → Unauthorized when the II default account was renamed

While setting up an app-scoped CLI identity with icp identity link web, I hit a failure that turned out to be reproducible for any app, and I wanted to share the root cause plus the workaround I ended up with, and ask whether there is a supported path I overlooked.

Symptom

icp identity link web my-identity --auth https://id.ai --app <app-domain>

The browser opens id.ai/cli and correctly shows the app domain. Clicking Allow access produces Unexpected error / Unauthorized, with the console stack pointing at onAuthorize.

What actually triggers it

The identity had its default account for that origin renamed in the II account list. That is the whole trigger; it is not specific to any app or derivation origin. With an untouched default account the command works first try.

Root cause

The CLI flow always requests the default account: account_number is hardcoded to [] in cli/utils.ts. Renaming a default account materialises it create_default_account allocates a real account number, sets config.default_account_number = Some(n), and overwrites the AccountReference whose account_number was None. But read_account resolves a None request by scanning for a reference whose account_number.is_none(), finds nothing, and returns None, which prepare_account_delegation maps to Unauthorized even though config.default_account_number points at exactly the account being asked for.

The principal is unaffected by the rename (a stored default keeps seed_from_anchor, so the seed is still calculate_anchor_seed(anchor, origin)), so this is purely a lookup-path miss. Filed with code links here: https://github.com/dfinity/internet-identity/issues/4171

Workaround

icp identity delegation request prints an SPKI public key and leaves the identity pending, and the classic #authorize postMessage protocol accepts an externally supplied sessionPublicKey. So the identity can be completed out of band:

  1. icp identity delegation request <name> → session public key (PEM SPKI).
  2. From a page whose origin is permitted for the target derivation origin, open an identity.ic0.app/#authorize popup and post { kind: "authorize-client", sessionPublicKey, maxTimeToLive, derivationOrigin }.
  3. Serialise the returned chain into the JSON shape icp identity delegation sign emits (hex publicKey, hex signature / pubkey, hex expiration).
  4. icp identity delegation use <name> --from-json <file>.

The classic flow still shows the account picker, so the right account gets selected and the resulting principal matches the one the web app derives. I have this wrapped in a script that verifies the resulting principal against the expected one and discards the identity on mismatch.

Questions

  1. Is there an existing, supported way to do this that I missed? some flag or flow that lets the CLI target a specific II account rather than the default?
  2. Is an --account selector on the CLI flow planned? Both prepare_account_delegation and get_account_delegation already take the argument; only the CLI page pins it to None.
  3. Is the classic #authorize postMessage protocol something I can keep depending on, or is it on a deprecation path in favour of the id.ai flows? If it is going away, I would rather not build on it.

Thanks in advance!

Hey @Ravenith,

thanks for flagging this!

We will look into the issue and inform you once we have a concrete plan on how to handle this scenario.

Best,
Marco

@Ravenith the II team will soon make a minimal fix so that the account marked as default is being used.

That’s nice! So we’ll be able to dynamically change the default account through the II GUI whenever we want to switch, allowing the selection to still work even if all the accounts have been renamed. Is that correct? Thanks for taking care of the issue

that is my interpretation, too.

so whatever account is selected as default at the point of linking the identity will be used, no matter how it is named.

watch out for proposals to upgrade the II canisters :wink:

maybe @b3hr4d or @4eku can give a heads up here once the proposal is live.

Thanks for the confirmation. Since we’re already on the topic, I’d like to take the opportunity to ask something that I’m not sure you can answer, but I’ll try anyway.

Is there any plan, or is it being considered, to add an optional proof-of-personhood feature inside Internet Identity, similar to WorldID (using a nullifier)?

It would be a huge improvement for dApps like OhShii or OpenChat, which currently need to integrate third-party services. For OhShii, it would be even more critical, because voting power will be tied to an anti-Sybil verification requirement. For robustness reasons, this cannot rely on authentication services like Google, as currently happens with MULTI/DEX.

Hi, happy to do that. I will post here once I see the fix in an II proposal.

I would like to see this on II too. Part of it already exists, so here is where things stand.

The file is vc_mvp.rs. prepare_id_alias takes a relying party and an issuer, makes one random alias, and signs two credentials with it. One says to the relying party “this alias is the user you know as X”. The other says to the issuer “this alias is the user you know as Y”. The two dapps see different principals for the same person, but during that one flow they can agree it is the same user. The alias is new on every call, and the credentials expire after 15 minutes. The issuer is also not told who the relying party is.

So the private linking part works. But it is not a nullifier. One identity can hold several accounts on the same origin, and each account gets its own principal. Counting principals counts pseudonyms, not people. The uniqueness would have to come from the issuer’s credential itself.

One thing worth knowing: prepare_id_alias and get_id_alias are live, but they are the old VC MVP. The code now marks them as legacy, and a newer attribute sharing protocol is taking over. So the idea is proven, but a personhood credential built today would probably use the newer one.

Either way, the missing piece is an issuer that actually attests personhood. I cannot say if that is planned, I am only reading the repo here, so it would be good to hear from the team.

I agree, and it’s great to know that a first part of the integration already exists. However, please keep in mind that a flow like the VC one does not prevent a whale from buying pre-verified identities.

For dapps that require a fresh verification attestation to validate highly critical actions (for example, governance-sensitive operations or recovery mechanisms), this verification should ideally not be tied only to the principal, as it currently is in DecideID. Instead, dapps should have the ability to require the user to recreate the proof again, allowing them to mitigate this type of attack as well.

This is a limitation that developers building identity-sensitive applications currently need to work around. For example, when designing OhShii’s decentralized governance architecture, we implemented a flow that will require fresh identity confirmation through World ID for these types of critical actions, not as a preference for one provider over another, but because this specific re-verification flow is not currently available through DecideID. In practice, the user’s previous verification status is removed by the dapp, requiring the user to provide a new and fresh proof before proceeding.

That is fair, and you are right that none of this stops someone buying a verified identity. II proves control of the passkey and nothing more. If someone sells their identity, the buyer passes every check the seller would.

On the principal binding, I think that describes DecideID rather than the VC flow itself. There is no stored verification status in the VC flow, so there is nothing for a dapp to remove. The alias is random on every flow and the issuer signs the credential to that alias. A credential from an earlier session points at a different alias, so it cannot be reused. prepare_credential is an update call on the issuer canister, so the issuer is asked live every time, and it also checks the alias is still in date.

So the workaround you described, clearing the old status to force a new proof, would not be needed here. The dapp would just require a new flow at the moment of the critical action.

The honest limit is that a fresh credential is not the same as a fresh liveness check. If the issuer re-signs without actually re-checking the person, the freshness is only on paper. The flow puts the issuer in a position to demand a real check, but it cannot make it do one.

The other gap is that there is no agreed way for a dapp to say “this proof must be under 60 seconds old”. Dapps do control the credential spec arguments, the demo issuer uses minAge that way, so a freshness argument could be added in the same shape. But an issuer would have to support it and nobody has standardised one.

Exactly. Yes, sorry if I made some inaccuracies, I was going from memory, but I see that the concept I wanted to convey came across.

The important point is that dApps need a way to remove a principal ID from their own records as “verified”. Once this happens, II should require the user to create a new Proof of Personhood for that specific dApp.

This is something we can currently solve with World ID, but not with DecideID.

I would also like to point out that if some dApps rely on DecideID as their only identity verification method, and if identity verification plays a central role in the dApp, any service disruption or potential sunset of the provider could have a major impact on the protocols that depend on it.

Adding multiple verification services can help limit the impact of such events and reduce user friction by offering more choice. However, it also partially weakens the intended Proof of Personhood effect if the goal is to ensure that each user has a single unique profile.

This is where a native Proof of Personhood feature integrated into Internet Identity could become valuable: it would reduce friction for users while relying on a core dfinity application.

No need to apologise, the point was clear. And a dapp can already clear its own records and demand a new flow today, so that part is in its hands. What is missing is an issuer that does a real check when asked again.

Your point on multiple providers is the sharpest one here and I think it is right. Uniqueness only holds inside one provider’s namespace. Accept two providers and one person can hold one profile per provider, so the sybil resistance quietly drops away. Redundancy for availability and uniqueness for governance pull against each other.

Which is a good argument for a single canonical issuer. Though native would not mean II checking anyone itself, there are no biometrics or document checks in the canister. It would be an external verifier doing the check with II attesting to the result, the same way verified_email is II attesting to something Google told it. The attribute list is short enough that a personhood attribute would sit beside it.

So the plumbing is not the hard part. Who does the human check and who is trusted to run it is the real question, and that is a governance call rather than an engineering one. Good thread, I have learned a fair bit from it.