diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 8acdbb7e87..f1b1cf6299 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -2,6 +2,7 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Validators; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; @@ -81,6 +82,8 @@ public class AccountsController : Controller private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> _organizationUserValidator; + private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> + _webauthnKeyValidator; public AccountsController( @@ -109,7 +112,8 @@ public class AccountsController : Controller IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> emergencyAccessValidator, IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> - organizationUserValidator + organizationUserValidator, + IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator ) { _cipherRepository = cipherRepository; @@ -136,6 +140,7 @@ public class AccountsController : Controller _sendValidator = sendValidator; _emergencyAccessValidator = emergencyAccessValidator; _organizationUserValidator = organizationUserValidator; + _webauthnKeyValidator = webAuthnKeyValidator; } #region DEPRECATED (Moved to Identity Service) @@ -442,7 +447,8 @@ public class AccountsController : Controller Folders = await _folderValidator.ValidateAsync(user, model.Folders), Sends = await _sendValidator.ValidateAsync(user, model.Sends), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), - OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys) + OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys), + WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.WebAuthnKeys) }; var result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel); diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index 437c1ba20d..a66055b97a 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -1,5 +1,5 @@ using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.Webauthn; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Response.WebAuthn; using Bit.Api.Models.Response; using Bit.Core; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs index cfeaec3248..d3cb5c2442 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; @@ -19,5 +20,6 @@ public class UpdateKeyRequestModel public IEnumerable<SendWithIdRequestModel> Sends { get; set; } public IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessKeys { get; set; } public IEnumerable<ResetPasswordWithOrgIdRequestModel> ResetPasswordKeys { get; set; } + public IEnumerable<WebAuthnLoginRotateKeyRequestModel> WebAuthnKeys { get; set; } } diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs index 2a3aa1dde9..8c6acbc8d4 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Utilities; using Fido2NetLib; -namespace Bit.Api.Auth.Models.Request.Webauthn; +namespace Bit.Api.Auth.Models.Request.WebAuthn; public class WebAuthnLoginCredentialCreateRequestModel { diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs index 1d2e0813ef..54244c2dbd 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Utilities; using Fido2NetLib; -namespace Bit.Api.Auth.Models.Request.Webauthn; +namespace Bit.Api.Auth.Models.Request.WebAuthn; public class WebAuthnLoginCredentialUpdateRequestModel { diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs new file mode 100644 index 0000000000..7e161cfbea --- /dev/null +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.Auth.Models.Request.WebAuthn; + +public class WebAuthnLoginRotateKeyRequestModel +{ + [Required] + public Guid Id { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } + + public WebAuthnLoginRotateKeyData ToWebAuthnRotateKeyData() + { + return new WebAuthnLoginRotateKeyData + { + Id = Id, + EncryptedUserKey = EncryptedUserKey, + EncryptedPublicKey = EncryptedPublicKey + }; + } + +} diff --git a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs index 01cf2559a6..3199dccd02 100644 --- a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs +++ b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Models.Api; +using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Response.WebAuthn; @@ -13,9 +14,17 @@ public class WebAuthnCredentialResponseModel : ResponseModel Id = credential.Id.ToString(); Name = credential.Name; PrfStatus = credential.GetPrfStatus(); + EncryptedUserKey = credential.EncryptedUserKey; + EncryptedPublicKey = credential.EncryptedPublicKey; } public string Id { get; set; } public string Name { get; set; } public WebAuthnPrfStatus PrfStatus { get; set; } + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } } diff --git a/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs b/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs new file mode 100644 index 0000000000..5c4d0ef302 --- /dev/null +++ b/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs @@ -0,0 +1,55 @@ +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Exceptions; + +namespace Bit.Api.Auth.Validators; + +public class WebAuthnLoginKeyRotationValidator : IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> +{ + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; + + public WebAuthnLoginKeyRotationValidator(IWebAuthnCredentialRepository webAuthnCredentialRepository) + { + _webAuthnCredentialRepository = webAuthnCredentialRepository; + } + + public async Task<IEnumerable<WebAuthnLoginRotateKeyData>> ValidateAsync(User user, IEnumerable<WebAuthnLoginRotateKeyRequestModel> keysToRotate) + { + // 2024-06: Remove after 3 releases, for backward compatibility + if (keysToRotate == null) + { + return new List<WebAuthnLoginRotateKeyData>(); + } + + var result = new List<WebAuthnLoginRotateKeyData>(); + var existing = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); + if (existing == null || !existing.Any()) + { + return result; + } + + foreach (var ea in existing) + { + var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == ea.Id); + if (keyToRotate == null) + { + throw new BadRequestException("All existing webauthn prf keys must be included in the rotation."); + } + + if (keyToRotate.EncryptedUserKey == null) + { + throw new BadRequestException("WebAuthn prf keys must have user-key during rotation."); + } + if (keyToRotate.EncryptedPublicKey == null) + { + throw new BadRequestException("WebAuthn prf keys must have public-key during rotation."); + } + + result.Add(keyToRotate.ToWebAuthnRotateKeyData()); + } + + return result; + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 63b1a3c3cd..fd2a4dbe6f 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -30,6 +30,8 @@ using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Models.Data; #if !OSS using Bit.Commercial.Core.SecretsManager; @@ -163,7 +165,9 @@ public class Startup .AddScoped<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> , OrganizationUserRotationValidator>(); - + services + .AddScoped<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>, + WebAuthnLoginKeyRotationValidator>(); // Services services.AddBaseServices(globalSettings); diff --git a/src/Core/Auth/Models/Data/RotateUserKeyData.cs b/src/Core/Auth/Models/Data/RotateUserKeyData.cs index 52c6514770..f361c2a2cc 100644 --- a/src/Core/Auth/Models/Data/RotateUserKeyData.cs +++ b/src/Core/Auth/Models/Data/RotateUserKeyData.cs @@ -15,4 +15,5 @@ public class RotateUserKeyData public IReadOnlyList<Send> Sends { get; set; } public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; } public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; } + public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; } } diff --git a/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs new file mode 100644 index 0000000000..40a096c474 --- /dev/null +++ b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; + +namespace Bit.Core.Auth.Models.Data; + +public class WebAuthnLoginRotateKeyData +{ + [Required] + public Guid Id { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } + +} diff --git a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs index 50f03744c5..1fab56d07a 100644 --- a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs +++ b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs @@ -1,4 +1,6 @@ using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Repositories; namespace Bit.Core.Auth.Repositories; @@ -8,4 +10,5 @@ public interface IWebAuthnCredentialRepository : IRepository<WebAuthnCredential, Task<WebAuthnCredential> GetByIdAsync(Guid id, Guid userId); Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId); Task<bool> UpdateAsync(WebAuthnCredential credential); + UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials); } diff --git a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs index a580629864..4c7ca20737 100644 --- a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs +++ b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; @@ -20,6 +21,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; + private readonly IWebAuthnCredentialRepository _credentialRepository; /// <summary> /// Instantiates a new <see cref="RotateUserKeyCommand"/> @@ -35,7 +37,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, - IPushNotificationService pushService, IdentityErrorDescriber errors) + IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository) { _userService = userService; _userRepository = userRepository; @@ -46,6 +48,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand _organizationUserRepository = organizationUserRepository; _pushService = pushService; _identityErrorDescriber = errors; + _credentialRepository = credentialRepository; } /// <inheritdoc /> @@ -68,7 +71,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand user.Key = model.Key; user.PrivateKey = model.PrivateKey; if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() || - model.OrganizationUsers.Any()) + model.OrganizationUsers.Any() || model.WebAuthnKeys.Any()) { List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new(); @@ -99,6 +102,11 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand _organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers)); } + if (model.WebAuthnKeys.Any()) + { + saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys)); + } + await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions); } else diff --git a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs index d159157c0e..85a7cc64ef 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,7 +1,10 @@ using System.Data; using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; using Dapper; using Microsoft.Data.SqlClient; @@ -55,4 +58,37 @@ public class WebAuthnCredentialRepository : Repository<WebAuthnCredential, Guid> return affectedRows > 0; } + + public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + const string sql = @" + UPDATE WC + SET + WC.[EncryptedPublicKey] = UW.[encryptedPublicKey], + WC.[EncryptedUserKey] = UW.[encryptedUserKey] + FROM + [dbo].[WebAuthnCredential] WC + INNER JOIN + OPENJSON(@JsonCredentials) + WITH ( + id UNIQUEIDENTIFIER, + encryptedPublicKey NVARCHAR(MAX), + encryptedUserKey NVARCHAR(MAX) + ) UW + ON UW.id = WC.Id + WHERE + WC.[UserId] = @UserId"; + + var jsonCredentials = CoreHelpers.ClassToJsonData(credentials); + + await connection.ExecuteAsync( + sql, + new { UserId = userId, JsonCredentials = jsonCredentials }, + transaction: transaction, + commandType: CommandType.Text); + }; + } + } diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs index cd3751a6d0..1499811880 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,5 +1,7 @@ using AutoMapper; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; @@ -56,4 +58,30 @@ public class WebAuthnCredentialRepository : Repository<Core.Auth.Entities.WebAut return true; } } + + public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials) + { + return async (_, _) => + { + var newCreds = credentials.ToList(); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var userWebauthnCredentials = await GetDbSet(dbContext) + .Where(wc => wc.Id == wc.Id) + .ToListAsync(); + var validUserWebauthnCredentials = userWebauthnCredentials + .Where(wc => newCreds.Any(nwc => nwc.Id == wc.Id)) + .Where(wc => wc.UserId == userId); + + foreach (var wc in validUserWebauthnCredentials) + { + var nwc = newCreds.First(eak => eak.Id == wc.Id); + wc.EncryptedPublicKey = nwc.EncryptedPublicKey; + wc.EncryptedUserKey = nwc.EncryptedUserKey; + } + + await dbContext.SaveChangesAsync(); + }; + } + } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 9b6566bf64..1dd8fe064d 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; @@ -11,6 +12,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; @@ -67,6 +69,8 @@ public class AccountsControllerTests : IDisposable private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> _resetPasswordValidator; + private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> + _webauthnKeyRotationValidator; public AccountsControllerTests() @@ -97,6 +101,7 @@ public class AccountsControllerTests : IDisposable _sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>(); _emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>(); + _webauthnKeyRotationValidator = Substitute.For<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>(); _resetPasswordValidator = Substitute .For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>(); @@ -125,7 +130,8 @@ public class AccountsControllerTests : IDisposable _folderValidator, _sendValidator, _emergencyAccessValidator, - _resetPasswordValidator + _resetPasswordValidator, + _webauthnKeyRotationValidator ); } diff --git a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs index 85b0b9cab7..702fc7764a 100644 --- a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs @@ -1,6 +1,6 @@ using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.Webauthn; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; diff --git a/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs b/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs new file mode 100644 index 0000000000..97eadcbdc3 --- /dev/null +++ b/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs @@ -0,0 +1,93 @@ +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Api.Auth.Validators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Auth.Validators; + +[SutProviderCustomize] +public class WebAuthnLoginKeyRotationValidatorTests +{ + [Theory] + [BitAutoData] + public async Task ValidateAsync_WrongWebAuthnKeys_Throws( + SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user, + IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData) + { + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + EncryptedPublicKey = e.EncryptedPublicKey, + EncryptedUserKey = e.EncryptedUserKey + }).ToList(); + + var data = new WebAuthnCredential + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000002"), + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data }); + + await Assert.ThrowsAsync<BadRequestException>(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_NullUserKey_Throws( + SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user, + IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData) + { + var guid = Guid.NewGuid(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedPublicKey = e.EncryptedPublicKey, + }).ToList(); + + var data = new WebAuthnCredential + { + Id = guid, + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data }); + + await Assert.ThrowsAsync<BadRequestException>(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + + + [Theory] + [BitAutoData] + public async Task ValidateAsync_NullPublicKey_Throws( + SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user, + IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData) + { + var guid = Guid.NewGuid(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedUserKey = e.EncryptedUserKey, + }).ToList(); + + var data = new WebAuthnCredential + { + Id = guid, + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data }); + + await Assert.ThrowsAsync<BadRequestException>(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + +} diff --git a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs b/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs index f97e55a674..41c78f4272 100644 --- a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs @@ -1,4 +1,6 @@ -using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.Entities; using Bit.Core.Services; @@ -19,6 +21,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(true); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); @@ -31,6 +43,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(false); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); @@ -43,6 +65,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(true); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } await sutProvider.Sut.RotateUserKeyAsync(user, model);