refactor(2fa-webauthn) [PM-29890]: Add delete WebAuthn credential command.

This commit is contained in:
enmande
2026-01-27 16:01:10 -05:00
parent 73157b3ab8
commit 281afa2460
6 changed files with 74 additions and 30 deletions

View File

@@ -42,6 +42,7 @@ public class TwoFactorController : Controller
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IStartTwoFactorWebAuthnRegistrationCommand _startTwoFactorWebAuthnRegistrationCommand;
private readonly ICompleteTwoFactorWebAuthnRegistrationCommand _completeTwoFactorWebAuthnRegistrationCommand;
private readonly IDeleteTwoFactorWebAuthnCredentialCommand _deleteTwoFactorWebAuthnCredentialCommand;
public TwoFactorController(
IUserService userService,
@@ -55,7 +56,8 @@ public class TwoFactorController : Controller
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
ITwoFactorEmailService twoFactorEmailService,
IStartTwoFactorWebAuthnRegistrationCommand startTwoFactorWebAuthnRegistrationCommand,
ICompleteTwoFactorWebAuthnRegistrationCommand completeTwoFactorWebAuthnRegistrationCommand)
ICompleteTwoFactorWebAuthnRegistrationCommand completeTwoFactorWebAuthnRegistrationCommand,
IDeleteTwoFactorWebAuthnCredentialCommand deleteTwoFactorWebAuthnCredentialCommand)
{
_userService = userService;
_organizationRepository = organizationRepository;
@@ -69,6 +71,7 @@ public class TwoFactorController : Controller
_twoFactorEmailService = twoFactorEmailService;
_startTwoFactorWebAuthnRegistrationCommand = startTwoFactorWebAuthnRegistrationCommand;
_completeTwoFactorWebAuthnRegistrationCommand = completeTwoFactorWebAuthnRegistrationCommand;
_deleteTwoFactorWebAuthnCredentialCommand = deleteTwoFactorWebAuthnCredentialCommand;
}
[HttpGet("")]
@@ -321,7 +324,18 @@ public class TwoFactorController : Controller
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
{
var user = await CheckAsync(model, false);
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
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.");
}
var response = new TwoFactorWebAuthnResponseModel(user);
return response;
}

View File

@@ -0,0 +1,16 @@
using Bit.Core.Entities;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
public interface IDeleteTwoFactorWebAuthnCredentialCommand
{
/// <summary>
/// Deletes a Two-factor WebAuthn credential by ID.
/// </summary>
/// <param name="user">The current user.</param>
/// <param name="id">ID of the credential to delete.</param>
/// <returns>Whether deletion was successful.</returns>
Task<bool> DeleteTwoFactorWebAuthnCredentialAsync(User user, int id);
}

View File

@@ -0,0 +1,41 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Entities;
using Bit.Core.Services;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
public class DeleteTwoFactorWebAuthnCredentialCommand : IDeleteTwoFactorWebAuthnCredentialCommand
{
private readonly IUserService _userService;
public DeleteTwoFactorWebAuthnCredentialCommand(IUserService userService)
{
_userService = userService;
}
public async Task<bool> DeleteTwoFactorWebAuthnCredentialAsync(User user, int id)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
return false;
}
var keyName = $"Key{id}";
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (!provider?.MetaData?.ContainsKey(keyName) ?? true)
{
return false;
}
if (provider.MetaData.Count < 2)
{
return false;
}
provider.MetaData.Remove(keyName);
providers[TwoFactorProviderType.WebAuthn] = provider;
user.SetTwoFactorProviders(providers);
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
return true;
}
}

View File

@@ -75,6 +75,7 @@ public static class UserServiceCollectionExtensions
services
.AddScoped<IStartTwoFactorWebAuthnRegistrationCommand,
StartTwoFactorWebAuthnRegistrationCommand>();
services.AddScoped<IDeleteTwoFactorWebAuthnCredentialCommand, DeleteTwoFactorWebAuthnCredentialCommand>();
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
}

View File

@@ -23,7 +23,6 @@ public interface IUserService
Task<IdentityResult> CreateUserAsync(User user);
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
Task SendMasterPasswordHintAsync(string email);
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
Task SendEmailVerificationAsync(User user);
Task<IdentityResult> ConfirmEmailAsync(User user, string token);
Task InitiateEmailChangeAsync(User user, string newEmail);

View File

@@ -344,33 +344,6 @@ public class UserService : UserManager<User>, IUserService
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
}
public async Task<bool> DeleteWebAuthnKeyAsync(User user, int id)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
return false;
}
var keyName = $"Key{id}";
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (!provider?.MetaData?.ContainsKey(keyName) ?? true)
{
return false;
}
if (provider.MetaData.Count < 2)
{
return false;
}
provider.MetaData.Remove(keyName);
providers[TwoFactorProviderType.WebAuthn] = provider;
user.SetTwoFactorProviders(providers);
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
return true;
}
public async Task SendEmailVerificationAsync(User user)
{
if (user.EmailVerified)