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.
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
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.
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.
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.
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.
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,
});
}