Error message
InvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.
Chromium's text is above; other browsers word it differently. The DOMException name `InvalidStateError` is the stable part.
WebAuthn navigator.credentials.create() with excludeCredentials — DOMException InvalidStateError (Chromium)
What it means
Registration was refused because the authenticator already holds one of the credentials you listed in `excludeCredentials`. Relying parties send that list precisely to prevent registering the same authenticator twice for one account, so this is usually the intended outcome — the user already has a working passkey on this device. Browsers only reveal it after the user has interacted with the authenticator.
Why it happens
The user clicked "Add a passkey" again on the same device
commonThe most common case. The account already has a credential on this authenticator; offering to sign in with it is the right response.
A synced passkey arrived on this device
commoniCloud Keychain, Google Password Manager and other passkey providers sync credentials. A passkey registered on the phone is present on the laptop too, so registering "the laptop" is excluded.
The exclude list contains credentials of other users
occasionalA bug that lists every credential (or those of another account sharing the device) blocks registrations that should succeed.
How to fix it
- 1.
Tell the user they are already set up
Treat InvalidStateError as "a passkey already exists here" and lead the user to sign in with it.
js try { const credential = await navigator.credentials.create({ publicKey: options }); await saveCredential(credential); } catch (e) { if (e.name === "InvalidStateError") { showMessage("This device already has a passkey for your account. You can use it to sign in."); } else if (e.name !== "NotAllowedError") { throw e; } } - 2.
Exclude only the current user's credentials
Build `excludeCredentials` from the signed-in user's stored credential IDs, and use a stable random `user.id` per account.
js // Server: exclude only the CURRENT user's credentials (base64url ids from your database). const options = await generateRegistrationOptions({ rpName: "Example", rpID, userName: user.email, userID: user.webauthnUserId, // stable per user, never the email excludeCredentials: user.passkeys.map((p) => ({ id: p.credentialId, transports: p.transports })), });