LayerZeroFault
passkey recovery

Fix: WebAuthn InvalidStateError & Multi-Device Passkey Credential ID Collisions

VV

Written by

Fact-Checked on September 11, 2026

Verified Expert

Fix: WebAuthn InvalidStateError & Multi-Device Passkey Credential ID Collisions

In modern Web3 applications leveraging Account Abstraction (ERC-4337) and embedded passkey signers (such as Coinbase Smart Wallet, Turnkey, Privy, or ZeroDev), providing seamless multi-device recovery is a primary architectural goal. Users are encouraged to register both a primary authenticator (e.g. Apple TouchID via iCloud Keychain) and a secondary fallback authenticator (e.g. Windows Hello TPM, Android biometrics, or an external FIDO2 YubiKey).

However, developers frequently encounter an abrupt client-side exception during the secondary registration flow:

DOMException: The authenticator has already registered such credentials.
Code: 11
Name: InvalidStateError

Alternatively, on the smart contract layer, registering the secondary device silently overwrites the primary passkey’s public key coordinates, leaving the primary device unable to sign subsequent UserOperations.

If you are debugging iframe communication barriers during passkey enrollment, see our guide on WebAuthn Permissions Policy publickey-credentials-create Revert.


Technical Mechanics: The excludeCredentials Mandate

Under Section 5.1.3 of the W3C WebAuthn Level 3 specification, the excludeCredentials parameter is designed to prevent a single physical authenticator from accumulating multiple distinct passkey credentials for the same Relying Party (RP) user account:

// The standard credential creation options passed to navigator.credentials.create()
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
  challenge: randomChallengeBuffer,
  rp: { name: "LayerZeroFault Smart Wallet", id: "layerzerofault.site" },
  user: {
    id: userUuidBytes,
    name: "user@layerzerofault.site",
    displayName: "Smart Account 0x742d..."
  },
  pubKeyCredParams: [{ alg: -7, type: "public-key" }], // ES256 (secp256r1)
  excludeCredentials: [
    {
      id: existingCredentialIdBytes, // ← THE CULPRIT
      type: "public-key",
      transports: ["internal"]
    }
  ],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    userVerification: "required"
  }
};

Placeholder: WebAuthn excludeCredentials Collision Architecture

When the browser invokes navigator.credentials.create():

  1. The browser passes the excludeCredentials list to the target authenticator.
  2. If the authenticator already holds any credential whose ID matches an entry in excludeCredentials, the authenticator must abort registration immediately and return a status code of InvalidStateError.
  3. If the frontend indiscriminately copies all existing user credentials into excludeCredentials without filtering by device context, any attempt to update permissions or create an alternative key on a synced ecosystem (like Apple iCloud Keychain across Mac and iPhone) hits this wall.

Smart Contract Vulnerability: Slot Overwrite vs Key Registry

On the smart contract side, modular Account Abstraction validators (such as ERC-7579 or custom P256 verification modules) often store passkey signers in a fixed storage slot:

// VULNERABLE CONTRACT PATTERN: Single-slot or naive indexing
contract VulnerablePasskeyValidator {
    struct WebAuthnPublicKey {
        uint256 pubKeyX;
        uint256 pubKeyY;
    }

    // Overwritten whenever a new device is registered!
    mapping(address => WebAuthnPublicKey) public accountPasskeys;

    function addSigner(bytes calldata signature, uint256 newX, uint256 newY) external {
        // Authenticate existing signer...
        accountPasskeys[msg.sender] = WebAuthnPublicKey(newX, newY);
    }
}

When the secondary device is registered, accountPasskeys[account] is overwritten. The user’s primary device (which still holds its passkey locally) attempts to sign a UserOperation, but the contract evaluates the P256 signature against the secondary device’s public key coordinates, resulting in an unrecoverable AA23 reverted error.


The Complete Remediation Protocol

Step 1: Intelligent Frontend Credential Exclusion

When adding a secondary authenticator, filter excludeCredentials based on the targeted authenticator attachment:

Placeholder: Multi-Device Key Registration Filter Pipeline

// SECURE PATTERN: Context-aware excludeCredentials builder
export function buildCreationOptions(
  userId: Uint8Array,
  targetAttachment: "platform" | "cross-platform",
  existingCredentials: Array<{ id: Uint8Array; attachment?: string; transports?: AuthenticatorTransport[] }>
): PublicKeyCredentialCreationOptions {
  
  // Only exclude credentials that belong to the SAME hardware category
  const filteredExclusions: PublicKeyCredentialDescriptor[] = existingCredentials
    .filter(cred => {
      // If registering a roaming security key (YubiKey), DO NOT exclude platform (TouchID/Windows Hello) keys
      if (targetAttachment === "cross-platform") {
        return cred.transports?.includes("usb") || cred.transports?.includes("nfc");
      }
      // If registering a platform passkey, only exclude platform credentials
      return cred.attachment === "platform" || cred.transports?.includes("internal");
    })
    .map(cred => ({
      id: cred.id,
      type: "public-key",
      transports: cred.transports
    }));

  return {
    challenge: crypto.getRandomValues(new Uint8Array(32)),
    rp: { id: window.location.hostname, name: "Modular Web3 Wallet" },
    user: { id: userId, name: "user-session", displayName: "Wallet Signer" },
    pubKeyCredParams: [{ alg: -7, type: "public-key" }],
    authenticatorSelection: {
      authenticatorAttachment: targetAttachment,
      userVerification: "required",
      residentKey: "preferred"
    },
    excludeCredentials: filteredExclusions
  };
}

Step 2: Deterministic Key Mapping in Smart Account Contracts

To support arbitrary secondary authenticators without collision, map public keys by their WebAuthn Credential ID hash:

// SECURE PATTERN: Credential ID-addressed Multi-Passkey Validator
contract SecureMultiPasskeyValidator {
    struct WebAuthnSigner {
        uint256 pubKeyX;
        uint256 pubKeyY;
        bool isActive;
    }

    // Mapping: Account => (keccak256(credentialId) => Signer)
    mapping(address => mapping(bytes32 => WebAuthnSigner)) public accountSigners;

    event PasskeyAdded(address indexed account, bytes32 indexed credHash, uint256 pubKeyX, uint256 pubKeyY);
    event PasskeyRevoked(address indexed account, bytes32 indexed credHash);

    function addPasskeySigner(
        bytes32 credHash,
        uint256 pubKeyX,
        uint256 pubKeyY
    ) external {
        require(msg.sender == address(this), "Only self-authorized execution");
        require(!accountSigners[msg.sender][credHash].isActive, "Signer already active");

        accountSigners[msg.sender][credHash] = WebAuthnSigner({
            pubKeyX: pubKeyX,
            pubKeyY: pubKeyY,
            isActive: true
        });

        emit PasskeyAdded(msg.sender, credHash, pubKeyX, pubKeyY);
    }
}

Defensive Engineering Checklist

  1. Catch InvalidStateError Gracefully: Treat InvalidStateError in your UI as an informative prompt (“This device or iCloud account already has a registered passkey”) rather than a generic fatal error.
  2. Dual-Curve Support: Allow the secondary signer to be either ES256 (secp256r1 via P256) or RS256 (for enterprise Windows Hello configurations) without disrupting existing keys.
  3. Recovery Quorum: Require confirmation from the primary passkey (or a social recovery guardian) before committing secondary passkey coordinates into on-chain contract state.

Fact-Checked by Victor Vance, Senior Smart Contract Security Analyst & Node Operator.

Partner Spotlight: Gate.io

Trade Securely on Gate.io

Don't risk your assets on centralized silos or unverified endpoints. Trade securely on Gate.io with deep liquidity and institutional-grade security protocols.

Claim $100 Sign-up Bonus

Official Partner Referral Link

Related Inquiries

Why does navigator.credentials.create() throw 'InvalidStateError: The authenticator has already registered such credentials'?

This error occurs when the relying party passes an excludeCredentials array containing a credential ID that is already present on the physical authenticator being queried. The browser specification mandates that if an excluded credential exists on the selected hardware/platform enclave, creation must abort with InvalidStateError to prevent duplicate passkey bloat.

How does this break multi-device smart contract account setups?

When a user attempts to add an Apple iCloud Keychain passkey as a backup to their existing YubiKey or Windows Hello passkey on the same account, dApps frequently query the current device using an unpartitioned excludeCredentials list. If the user's browser attempts to register on the same synced iCloud Keychain, the platform enclave recognizes the existing credential and immediately terminates execution.

How can dApps safely register secondary passkeys for modular accounts?

By specifying transport constraints in excludeCredentials, dynamically filtering credentials based on authenticatorAttachment ('platform' vs 'cross-platform'), and storing public key coordinates in smart contracts using deterministic key hashes rather than raw sequential slots.