LayerZeroFault
passkey recovery Updated:

WebAuthn InvalidStateError & Multi-Device Passkey Credential ID Collisions

VV

Written by

Updated:

Technical Guide

To fix WebAuthn InvalidStateError during secondary passkey registration, exclude previously registered credential IDs from the excludeCredentials list or verify the user is registering on a distinct authenticator. An InvalidStateError is thrown by the WebAuthn API when navigator.credentials.create() detects that a credential ID passed in excludeCredentials already resides on the selected authenticator, terminating registration.

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.


[!TIP] Diagnostic Sandbox: Test WebAuthn registration options and simulate excludeCredentials arrays in our air-gapped WebAuthn Passkey Debugger. See also our guide on WebAuthn Conditional Mediation AbortError Fixes.

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"
  }
};

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:

// 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.

Interactive Diagnostic Sandbox

Local WebAuthn & Smart Account Sandboxes

Debug WebAuthn credential requests, PRF extensions, and simulate ERC-4337 passkey account validation in client WebCrypto.

Companion Diagnostic Guide WebAuthn publickey-credentials-create Permissions Policy Revert →

Resolve WebAuthn passkey registration blocks in embedded wallets. Fix 'Access to this feature is blocked by permissions policy: publickey-credentials-create'.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

What causes an InvalidStateError during passkey registration?

WebAuthn returns InvalidStateError when an authenticator already contains a credential for the specified RP ID that matches one of the IDs listed in the excludeCredentials creation options.

How do you allow users to register multiple passkeys for the same account?

Only populate excludeCredentials with credentials known to reside on the same physical device, or inform users to select a different authenticator (e.g., an external YubiKey instead of iCloud Keychain).

Can duplicate credential IDs cause smart wallet signature conflicts?

Yes. If an application registers identical credential IDs for multiple recovery signers, on-chain signature verification may fail or map to the wrong key index in the smart account validator.

How can developers test excludeCredentials parameters?

Simulate WebAuthn registration calls and inspect excludeCredentials parameter behavior directly in our local WebAuthn & Passkey Debugger tool.