bitwarden/server · error · NotFoundException
Credential not found.
Error message
Credential not found.
What it means
Thrown (HTTP 404) by DELETE /webauthn/{id}/delete when _credentialRepository.GetByIdAsync(id, user.Id) returns null. The lookup is scoped by both credential id and the authenticated user id, so an id that belongs to a different user also yields 404, not 403.
Source
Thrown at src/Api/Auth/Controllers/WebAuthnController.cs:165
}
// assign new keys to credential
credential.EncryptedUserKey = model.EncryptedUserKey;
credential.EncryptedPrivateKey = model.EncryptedPrivateKey;
credential.EncryptedPublicKey = model.EncryptedPublicKey;
await _credentialRepository.UpdateAsync(credential);
}
[Authorize(Policies.Web)]
[HttpPost("{id}/delete")]
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
{
var user = await VerifyUserAsync(model);
var credential = await _credentialRepository.GetByIdAsync(id, user.Id);
if (credential == null)
{
throw new NotFoundException("Credential not found.");
}
await _credentialRepository.DeleteAsync(credential);
}
private async Task<Core.Entities.User> GetUserAsync()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
return user;
}
private async Task<Core.Entities.User> VerifyUserAsync(SecretVerificationRequestModel model)
{
var user = await GetUserAsync();View on GitHub (pinned to e93b962371)
Solutions
- Refresh the WebAuthn credential list and retry with a current id.
- Treat a 404 on this endpoint as an idempotent success in the UI (the credential is already gone).
- Confirm the id originates from the current user's own credential list.
Example fix
// before: surface 404 as an error to the user
catch (ex) { showError('Delete failed'); }
// after: treat 404 as already-deleted success
if (resp.status === 404) { markCredentialRemoved(id); return; } Defensive patterns
Strategy: validation
Validate before calling
// Client-side: confirm the id is still in the user's credential list before issuing the delete.
const current = await getCredentialList();
if (!current.some(c => c.id === deleteId)) { markRemoved(deleteId); return; } Try / catch
// Treat 404 from this endpoint as an idempotent success.
try {
await del(`/webauthn/${id}/delete`, body);
} catch (e) {
if (e.status === 404) { markCredentialRemoved(id); return; }
throw e;
} Prevention
- Refresh the credential list after any delete before offering further deletes.
- Make delete idempotent in the UI by treating 404 as success.
- Guard against concurrent deletes with optimistic locking or a post-action refresh.
When it happens
Trigger: Client submits a credential id that was already deleted; the id belongs to another user; a stale id from an out-of-sync UI list; a concurrent delete from another session/device between listing and deleting.
Common situations: Credential list not refreshed after a delete on another device; user pasted an old id; race condition between two clients deleting the same passkey.
Related errors
- User not found.
- Group not found.
- User not found.
- Resource not found.
- Unable to delete WebAuthn credential.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/a4c38e6b392bab28.
Report an issue: GitHub.