demo · v130

signalUnknownCredential — stale passkey cleanup

The chromestatus motivation: a user deletes their account on a site, but the site has no way to tell the password manager / OS credential provider to forget the passkey. The next time the user signs in to the site (with a new account), their old passkey still shows up in the picker as a discoverable credential. PublicKeyCredential.signalUnknownCredential() closes the loop.

requires Chrome 130 with WebAuthn signal flag Enable chrome://flags/#enable-experimental-web-platform-features and reload if the method is missing. The buttons below use the backend credential store and only report success when Chrome accepts the real call.

the user journey

1

account deletion

User opens shop.example/account and clicks Delete account. The relying party wipes the user record in its DB; the WebAuthn credential ID stored against that user is also forgotten.

2

password manager is unaware

The user's password manager (1Password, iCloud Keychain, Chrome's profile sync) still has the passkey for shop.example linked to the now-deleted username. There's no signal from the site to drop it.

3

user makes a new account

Two days later the user signs up again, creates a new passkey, completes the flow. The new passkey is registered fine.

4

next sign-in: ghost in the picker

Next time they tap Sign in with passkey, the picker shows two shop.example credentials — one for their deleted account, one for the new one. The first one will fail because the server doesn't recognise it.

5

Chrome 130: signal the cleanup

On account deletion (step 1), the relying party calls PublicKeyCredential.signalUnknownCredential() with the credential ID. The browser forwards the hint to the credential provider; the next picker only shows the live passkey.

the picker before vs after

without signal — picker after step 4

  • old-user · cred-id 0xdead…stale
  • new-user · cred-id 0xc0fe…live

user picks first one — auth fails. Frustration; support ticket.

with signalUnknownCredential — picker after step 5

  • new-user · cred-id 0xc0fe…live

stale credential removed proactively. Single-tap sign-in works.

the code

// In your delete-account handler, after wiping the user record:
await fetch("/api/account", { method: "DELETE" });

if ("signalUnknownCredential" in PublicKeyCredential) {
  await PublicKeyCredential.signalUnknownCredential({
    rpId: "shop.example",
    credentialId: deletedCredentialIdBase64Url,
  });
}

// On every successful sign-in, also reconcile the full known list:
if ("signalAllAcceptedCredentials" in PublicKeyCredential) {
  await PublicKeyCredential.signalAllAcceptedCredentials({
    rpId: "shop.example",
    userId: currentUserIdBase64Url,
    allAcceptedCredentialIds: [
      activeCredentialId1, activeCredentialId2,
    ],
  });
}

// And when the user updates their display name in account settings:
if ("signalCurrentUserDetails" in PublicKeyCredential) {
  await PublicKeyCredential.signalCurrentUserDetails({
    rpId: "shop.example",
    userId: currentUserIdBase64Url,
    name: newUsername,
    displayName: newDisplayName,
  });
}

see also