bitwarden/server · error · BadRequestException

Unable to delete WebAuthn credential.

Error message

Unable to delete WebAuthn credential.

What it means

BadRequestException 'Unable to delete WebAuthn credential.' is thrown in DELETE /webauthn (DeleteWebAuthn) when model.Id has no value (null/empty guid). The delete command requires a concrete credential id; without it the request is rejected before the command runs.

Source

Thrown at src/Api/Auth/Controllers/TwoFactorController.cs:382

        return new TwoFactorWebAuthnUpdateResponseModel(user);
    }

    [HttpPost("webauthn")]
    [Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")]
    public async Task<TwoFactorWebAuthnUpdateResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnUpdateRequestModel model)
    {
        return await PutWebAuthn(model);
    }

    [HttpDelete("webauthn")]
    public async Task<TwoFactorWebAuthnDeleteResponseModel> DeleteWebAuthn(
        [FromBody] TwoFactorWebAuthnDeleteRequestModel model)
    {
        var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);

        if (!model.Id.HasValue)
        {
            throw new BadRequestException("Unable to delete WebAuthn credential.");
        }

        var success = await _deleteTwoFactorWebAuthnCredentialCommand.DeleteTwoFactorWebAuthnCredentialAsync(user, model.Id.Value);
        if (!success)
        {
            throw new BadRequestException("Unable to delete WebAuthn credential.");
        }

        return new TwoFactorWebAuthnDeleteResponseModel(user);
    }

    [HttpDelete("webauthn/all")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DeleteWebAuthnAll(
        [FromBody] TwoFactorWebAuthnDeleteAllRequestModel model)
    {
        var user = await ValidateUserVerificationTokenAsync(model.UserVerificationToken, TwoFactorProviderType.WebAuthn);
        await _userService.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);

View on GitHub (pinned to e93b962371)

Solutions

  1. Include a non-empty Guid in the Id field of the delete request body.
  2. Verify JSON property casing matches the request model (Id vs id) and that serialization is not dropping nulls.
  3. Fetch the credential list first and pass the selected credential's Id.

Example fix

// before
api.delete('/users/two-factor/webauthn', { data: { userVerificationToken } }) // missing id
// after
api.delete('/users/two-factor/webauthn', { data: { userVerificationToken, id: selectedCredentialId } });
Defensive patterns

Strategy: validation

Validate before calling

if (!model.id) throw new Error('A WebAuthn credential id is required to delete');

Type guard

function hasCredentialId(m): m is { id: string } { return !!m?.id && typeof m.id === 'string'; }

Try / catch

try { await api.delete('/users/two-factor/webauthn', { data: model }); }
catch (e) {
  if (e.response?.status === 400 && /delete WebAuthn/.test(e.response.data?.message) && !model.id) {
    throw new UserFacingError('Select a credential to delete.');
  }
  throw e;
}

Prevention

When it happens

Trigger: DELETE /api/users/two-factor/webauthn (TwoFactorController line 382) where the request body omits Id or sends it as null. The !model.Id.HasValue guard fires.

Common situations: Client serializes the credential id as null, sends an empty body, or references a credential list that has no selected id. A deserialization mismatch (wrong casing) can also leave Id null.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/4d9f5a84161da3a2. Report an issue: GitHub.