diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index a30e83a5e..151499df7 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Tokens; @@ -25,17 +26,23 @@ public class WebAuthnController : Controller private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IDataProtectorTokenFactory _createOptionsDataProtector; private readonly IPolicyService _policyService; + private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand; + private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand; public WebAuthnController( IUserService userService, IWebAuthnCredentialRepository credentialRepository, IDataProtectorTokenFactory createOptionsDataProtector, - IPolicyService policyService) + IPolicyService policyService, + IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand, + ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand) { _userService = userService; _credentialRepository = credentialRepository; _createOptionsDataProtector = createOptionsDataProtector; _policyService = policyService; + _getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand; + _createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand; } [HttpGet("")] @@ -52,7 +59,7 @@ public class WebAuthnController : Controller { var user = await VerifyUserAsync(model); await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id); - var options = await _userService.StartWebAuthnLoginRegistrationAsync(user); + var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user); var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options); var token = _createOptionsDataProtector.Protect(tokenable); @@ -76,7 +83,7 @@ public class WebAuthnController : Controller throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue."); } - var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey); + var success = await _createWebAuthnLoginCredentialCommand.CreateWebAuthnLoginCredentialAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey); if (!success) { throw new BadRequestException("Unable to complete WebAuthn registration."); diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index eff162c73..3f5171ca2 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -2,6 +2,8 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +16,7 @@ public static class UserServiceCollectionExtensions { services.AddScoped(); services.AddUserPasswordCommands(); + services.AddWebAuthnLoginCommands(); } private static void AddUserPasswordCommands(this IServiceCollection services) @@ -21,4 +24,11 @@ public static class UserServiceCollectionExtensions services.AddScoped(); } + private static void AddWebAuthnLoginCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } } diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/IAssertWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/IAssertWebAuthnLoginCredentialCommand.cs new file mode 100644 index 000000000..c149ae5a4 --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/IAssertWebAuthnLoginCredentialCommand.cs @@ -0,0 +1,10 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Entities; +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin; + +public interface IAssertWebAuthnLoginCredentialCommand +{ + public Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse); +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs new file mode 100644 index 000000000..c25e226a3 --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Entities; +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin; + +public interface ICreateWebAuthnLoginCredentialCommand +{ + public Task CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null); +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialAssertionOptionsCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialAssertionOptionsCommand.cs new file mode 100644 index 000000000..afaefe529 --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialAssertionOptionsCommand.cs @@ -0,0 +1,8 @@ +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin; + +public interface IGetWebAuthnLoginCredentialAssertionOptionsCommand +{ + public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions(); +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialCreateOptionsCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialCreateOptionsCommand.cs new file mode 100644 index 000000000..4b8ee8c3a --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialCreateOptionsCommand.cs @@ -0,0 +1,12 @@ +using Bit.Core.Entities; +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin; + +/// +/// Get the options required to create a Passkey for login. +/// +public interface IGetWebAuthnLoginCredentialCreateOptionsCommand +{ + public Task GetWebAuthnLoginCredentialCreateOptionsAsync(User user); +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/AssertWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/AssertWebAuthnLoginCredentialCommand.cs new file mode 100644 index 000000000..88e5ec32b --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/AssertWebAuthnLoginCredentialCommand.cs @@ -0,0 +1,63 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.Utilities; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; + +internal class AssertWebAuthnLoginCredentialCommand : IAssertWebAuthnLoginCredentialCommand +{ + private readonly IFido2 _fido2; + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; + private readonly IUserRepository _userRepository; + + public AssertWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository, IUserRepository userRepository) + { + _fido2 = fido2; + _webAuthnCredentialRepository = webAuthnCredentialRepository; + _userRepository = userRepository; + } + + public async Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse) + { + if (!GuidUtilities.TryParseBytes(assertionResponse.Response.UserHandle, out var userId)) + { + throw new BadRequestException("Invalid credential."); + } + + var user = await _userRepository.GetByIdAsync(userId); + if (user == null) + { + throw new BadRequestException("Invalid credential."); + } + + var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); + var assertedCredentialId = CoreHelpers.Base64UrlEncode(assertionResponse.Id); + var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertedCredentialId); + if (credential == null) + { + throw new BadRequestException("Invalid credential."); + } + + // Always return true, since we've already filtered the credentials after user id + IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true); + var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey); + var assertionVerificationResult = await _fido2.MakeAssertionAsync( + assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback); + + // Update SignatureCounter + credential.Counter = (int)assertionVerificationResult.Counter; + await _webAuthnCredentialRepository.ReplaceAsync(credential); + + if (assertionVerificationResult.Status != "ok") + { + throw new BadRequestException("Invalid credential."); + } + + return (user, credential); + } +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs new file mode 100644 index 000000000..65c98dea3 --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs @@ -0,0 +1,53 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Fido2NetLib; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; + +internal class CreateWebAuthnLoginCredentialCommand : ICreateWebAuthnLoginCredentialCommand +{ + public const int MaxCredentialsPerUser = 5; + + private readonly IFido2 _fido2; + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; + + public CreateWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository) + { + _fido2 = fido2; + _webAuthnCredentialRepository = webAuthnCredentialRepository; + } + + public async Task CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null) + { + var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); + if (existingCredentials.Count >= MaxCredentialsPerUser) + { + return false; + } + + var existingCredentialIds = existingCredentials.Select(c => c.CredentialId); + IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId))); + + var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); + + var credential = new WebAuthnCredential + { + Name = name, + CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId), + PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey), + Type = success.Result.CredType, + AaGuid = success.Result.Aaguid, + Counter = (int)success.Result.Counter, + UserId = user.Id, + SupportsPrf = supportsPrf, + EncryptedUserKey = encryptedUserKey, + EncryptedPublicKey = encryptedPublicKey, + EncryptedPrivateKey = encryptedPrivateKey + }; + + await _webAuthnCredentialRepository.CreateAsync(credential); + return true; + } +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialAssertionOptionsCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialAssertionOptionsCommand.cs new file mode 100644 index 000000000..3456de4dd --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialAssertionOptionsCommand.cs @@ -0,0 +1,19 @@ +using Fido2NetLib; +using Fido2NetLib.Objects; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; + +internal class GetWebAuthnLoginCredentialAssertionOptionsCommand : IGetWebAuthnLoginCredentialAssertionOptionsCommand +{ + private readonly IFido2 _fido2; + + public GetWebAuthnLoginCredentialAssertionOptionsCommand(IFido2 fido2) + { + _fido2 = fido2; + } + + public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions() + { + return _fido2.GetAssertionOptions(Enumerable.Empty(), UserVerificationRequirement.Required); + } +} diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialCreateOptionsCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialCreateOptionsCommand.cs new file mode 100644 index 000000000..1d47afb03 --- /dev/null +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialCreateOptionsCommand.cs @@ -0,0 +1,49 @@ +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Fido2NetLib; +using Fido2NetLib.Objects; + +namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; + +internal class GetWebAuthnLoginCredentialCreateOptionsCommand : IGetWebAuthnLoginCredentialCreateOptionsCommand +{ + private readonly IFido2 _fido2; + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; + + public GetWebAuthnLoginCredentialCreateOptionsCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository) + { + _fido2 = fido2; + _webAuthnCredentialRepository = webAuthnCredentialRepository; + } + + public async Task GetWebAuthnLoginCredentialCreateOptionsAsync(User user) + { + var fidoUser = new Fido2User + { + DisplayName = user.Name, + Name = user.Email, + Id = user.Id.ToByteArray(), + }; + + // Get existing keys to exclude + var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); + var excludeCredentials = existingKeys + .Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId))) + .ToList(); + + var authenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = null, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required + }; + + var extensions = new AuthenticationExtensionsClientInputs { }; + + var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection, + AttestationConveyancePreference.None, extensions); + + return options; + } +} diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index c9ccef661..c789e2d4f 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -1,5 +1,4 @@ using System.Security.Claims; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; @@ -28,10 +27,6 @@ public interface IUserService Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); - Task StartWebAuthnLoginRegistrationAsync(User user); - Task CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null); - AssertionOptions StartWebAuthnLoginAssertion(); - Task<(User, WebAuthnCredential)> CompleteWebAuthLoginAssertionAsync(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse); Task SendEmailVerificationAsync(User user); Task ConfirmEmailAsync(User user, string token); Task InitiateEmailChangeAsync(User user, string newEmail); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 81ce69762..804b1bea2 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -3,12 +3,9 @@ using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -65,7 +62,6 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IProviderUserRepository _providerUserRepository; private readonly IStripeSyncService _stripeSyncService; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; - private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; public UserService( IUserRepository userRepository, @@ -97,8 +93,7 @@ public class UserService : UserManager, IUserService, IDisposable IAcceptOrgUserCommand acceptOrgUserCommand, IProviderUserRepository providerUserRepository, IStripeSyncService stripeSyncService, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IWebAuthnCredentialRepository webAuthnRepository) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory) : base( store, optionsAccessor, @@ -136,7 +131,6 @@ public class UserService : UserManager, IUserService, IDisposable _providerUserRepository = providerUserRepository; _stripeSyncService = stripeSyncService; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _webAuthnCredentialRepository = webAuthnRepository; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -522,114 +516,6 @@ public class UserService : UserManager, IUserService, IDisposable return true; } - public async Task StartWebAuthnLoginRegistrationAsync(User user) - { - var fidoUser = new Fido2User - { - DisplayName = user.Name, - Name = user.Email, - Id = user.Id.ToByteArray(), - }; - - // Get existing keys to exclude - var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); - var excludeCredentials = existingKeys - .Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId))) - .ToList(); - - var authenticatorSelection = new AuthenticatorSelection - { - AuthenticatorAttachment = null, - RequireResidentKey = true, - UserVerification = UserVerificationRequirement.Required - }; - - var extensions = new AuthenticationExtensionsClientInputs { }; - - var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection, - AttestationConveyancePreference.None, extensions); - - return options; - } - - public async Task CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, - AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, - string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null) - { - var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); - if (existingCredentials.Count >= 5) - { - return false; - } - - var existingCredentialIds = existingCredentials.Select(c => c.CredentialId); - IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId))); - - var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback); - - var credential = new WebAuthnCredential - { - Name = name, - CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId), - PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey), - Type = success.Result.CredType, - AaGuid = success.Result.Aaguid, - Counter = (int)success.Result.Counter, - UserId = user.Id, - SupportsPrf = supportsPrf, - EncryptedUserKey = encryptedUserKey, - EncryptedPublicKey = encryptedPublicKey, - EncryptedPrivateKey = encryptedPrivateKey - }; - - await _webAuthnCredentialRepository.CreateAsync(credential); - return true; - } - - public AssertionOptions StartWebAuthnLoginAssertion() - { - return _fido2.GetAssertionOptions(Enumerable.Empty(), UserVerificationRequirement.Required); - } - - public async Task<(User, WebAuthnCredential)> CompleteWebAuthLoginAssertionAsync(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse) - { - if (!GuidUtilities.TryParseBytes(assertionResponse.Response.UserHandle, out var userId)) - { - throw new BadRequestException("Invalid credential."); - } - - var user = await _userRepository.GetByIdAsync(userId); - if (user == null) - { - throw new BadRequestException("Invalid credential."); - } - - var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); - var assertedCredentialId = CoreHelpers.Base64UrlEncode(assertionResponse.Id); - var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertedCredentialId); - if (credential == null) - { - throw new BadRequestException("Invalid credential."); - } - - // Always return true, since we've already filtered the credentials after user id - IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true); - var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey); - var assertionVerificationResult = await _fido2.MakeAssertionAsync( - assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback); - - // Update SignatureCounter - credential.Counter = (int)assertionVerificationResult.Counter; - await _webAuthnCredentialRepository.ReplaceAsync(credential); - - if (assertionVerificationResult.Status != "ok") - { - throw new BadRequestException("Invalid credential."); - } - - return (user, credential); - } - public async Task SendEmailVerificationAsync(User user) { if (user.EmailVerified) diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 9469673e1..fe91eeede 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -4,6 +4,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Services; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Auth.Utilities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -26,20 +27,22 @@ public class AccountsController : Controller private readonly IUserService _userService; private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; - + private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; public AccountsController( ILogger logger, IUserRepository userRepository, IUserService userService, ICaptchaValidationService captchaValidationService, - IDataProtectorTokenFactory assertionOptionsDataProtector) + IDataProtectorTokenFactory assertionOptionsDataProtector, + IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand) { _logger = logger; _userRepository = userRepository; _userService = userService; _captchaValidationService = captchaValidationService; _assertionOptionsDataProtector = assertionOptionsDataProtector; + _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; } // Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints. @@ -85,7 +88,7 @@ public class AccountsController : Controller [RequireFeature(FeatureFlagKeys.PasswordlessLogin)] public WebAuthnLoginAssertionOptionsResponseModel GetWebAuthnLoginAssertionOptions() { - var options = _userService.StartWebAuthnLoginAssertion(); + var options = _getWebAuthnLoginCredentialAssertionOptionsCommand.GetWebAuthnLoginCredentialAssertionOptions(); var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.Authentication, options); var token = _assertionOptionsDataProtector.Protect(tokenable); diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 6168ac22b..5928974d5 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -7,6 +7,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; @@ -26,6 +27,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator _assertionOptionsDataProtector; + private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand; public WebAuthnGrantValidator( UserManager userManager, @@ -48,7 +50,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator assertionOptionsDataProtector, IFeatureService featureService, IDistributedCache distributedCache, - IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder + IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, + IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, @@ -56,6 +59,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator "webauthn"; @@ -86,7 +90,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator() .GetUserByPrincipalAsync(default) .ReturnsForAnyArgs(user); - sutProvider.GetDependency() - .CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey) + sutProvider.GetDependency() + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey) .Returns(true); sutProvider.GetDependency>() .Unprotect(requestModel.Token) @@ -142,8 +143,8 @@ public class WebAuthnControllerTests sutProvider.GetDependency() .GetUserByPrincipalAsync(default) .ReturnsForAnyArgs(user); - sutProvider.GetDependency() - .CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any(), false) + sutProvider.GetDependency() + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), false) .Returns(true); sutProvider.GetDependency>() .Unprotect(requestModel.Token) diff --git a/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/AssertWebAuthnLoginCredentialCommandTests.cs b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/AssertWebAuthnLoginCredentialCommandTests.cs new file mode 100644 index 000000000..22fdeeee2 --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/AssertWebAuthnLoginCredentialCommandTests.cs @@ -0,0 +1,107 @@ +using System.Text; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Fido2NetLib; +using Fido2NetLib.Objects; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin; + +[SutProviderCustomize] +public class AssertWebAuthnLoginCredentialCommandTests +{ + [Theory, BitAutoData] + internal async void InvalidUserHandle_ThrowsBadRequestException(SutProvider sutProvider, AssertionOptions options, AuthenticatorAssertionRawResponse response) + { + // Arrange + response.Response.UserHandle = Encoding.UTF8.GetBytes("invalid-user-handle"); + + // Act + var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + internal async void UserNotFound_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response) + { + // Arrange + response.Response.UserHandle = user.Id.ToByteArray(); + sutProvider.GetDependency().GetByIdAsync(user.Id).ReturnsNull(); + + // Act + var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + internal async void NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response) + { + // Arrange + response.Response.UserHandle = user.Id.ToByteArray(); + sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { }); + + // Act + var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + internal async void AssertionFails_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult) + { + // Arrange + var credentialId = Guid.NewGuid().ToByteArray(); + credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId); + response.Id = credentialId; + response.Response.UserHandle = user.Id.ToByteArray(); + assertionResult.Status = "Not ok"; + sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential }); + sutProvider.GetDependency().MakeAssertionAsync(response, options, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(assertionResult); + + // Act + var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + internal async void AssertionSucceeds_ReturnsUserAndCredential(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult) + { + // Arrange + var credentialId = Guid.NewGuid().ToByteArray(); + credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId); + response.Id = credentialId; + response.Response.UserHandle = user.Id.ToByteArray(); + assertionResult.Status = "ok"; + sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential }); + sutProvider.GetDependency().MakeAssertionAsync(response, options, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(assertionResult); + + // Act + var result = await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response); + + // Assert + var (userResult, credentialResult) = result; + Assert.Equal(user, userResult); + Assert.Equal(credential, credentialResult); + } +} diff --git a/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/CreateWebAuthnLoginCredentialCommandTests.cs b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/CreateWebAuthnLoginCredentialCommandTests.cs new file mode 100644 index 000000000..8fbb68e59 --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/CreateWebAuthnLoginCredentialCommandTests.cs @@ -0,0 +1,65 @@ +using AutoFixture; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; +using Bit.Core.Entities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Fido2NetLib; +using Fido2NetLib.Objects; +using NSubstitute; +using Xunit; +using static Fido2NetLib.Fido2; + +namespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin; + +[SutProviderCustomize] +public class CreateWebAuthnLoginCredentialCommandTests +{ + [Theory, BitAutoData] + internal async void ExceedsExistingCredentialsLimit_ReturnsFalse(SutProvider sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator credentialGenerator) + { + // Arrange + var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser).ToList(); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(existingCredentials); + + // Act + var result = await sutProvider.Sut.CreateWebAuthnLoginCredentialAsync(user, "name", options, response, false, null, null, null); + + // Assert + Assert.False(result); + await sutProvider.GetDependency().DidNotReceive().CreateAsync(Arg.Any()); + } + + [Theory, BitAutoData] + internal async void DoesNotExceedExistingCredentialsLimit_CreatesCredential(SutProvider sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator credentialGenerator) + { + // Arrange + var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser - 1).ToList(); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(existingCredentials); + sutProvider.GetDependency().MakeNewCredentialAsync( + response, options, Arg.Any(), Arg.Any(), Arg.Any() + ).Returns(MakeCredentialResult()); + + // Act + var result = await sutProvider.Sut.CreateWebAuthnLoginCredentialAsync(user, "name", options, response, false, null, null, null); + + // Assert + Assert.True(result); + await sutProvider.GetDependency().Received().CreateAsync(Arg.Any()); + } + + private CredentialMakeResult MakeCredentialResult() + { + return new CredentialMakeResult("ok", "", new AttestationVerificationSuccess + { + Aaguid = new Guid(), + Counter = 0, + CredentialId = new Guid().ToByteArray(), + CredType = "public-key", + PublicKey = new byte[0], + Status = "ok", + User = new Fido2User(), + }); + } +} diff --git a/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/GetWebAuthnLoginCredentialCreateOptionsCommandTests.cs b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/GetWebAuthnLoginCredentialCreateOptionsCommandTests.cs new file mode 100644 index 000000000..f444ba9be --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/WebAuthnLogin/GetWebAuthnLoginCredentialCreateOptionsCommandTests.cs @@ -0,0 +1,60 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; +using Bit.Core.Entities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Fido2NetLib; +using Fido2NetLib.Objects; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin; + +[SutProviderCustomize] +public class GetWebAuthnLoginCredentialCreateOptionsTests +{ + [Theory, BitAutoData] + internal async Task NoExistingCredentials_ReturnsOptionsWithoutExcludedCredentials(SutProvider sutProvider, User user) + { + // Arrange + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns(new List()); + + // Act + var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user); + + // Assert + sutProvider.GetDependency() + .Received() + .RequestNewCredential( + Arg.Any(), + Arg.Is>(list => list.Count == 0), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory, BitAutoData] + internal async Task HasExistingCredential_ReturnsOptionsWithExcludedCredential(SutProvider sutProvider, User user, WebAuthnCredential credential) + { + // Arrange + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns(new List { credential }); + + // Act + var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user); + + // Assert + sutProvider.GetDependency() + .Received() + .RequestNewCredential( + Arg.Any(), + Arg.Is>(list => list.Count == 1), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } +} diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index c77b0bf7f..e66de5698 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,17 +1,12 @@ -using System.Text; -using System.Text.Json; -using AutoFixture; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; -using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -19,21 +14,18 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Services; -using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using Bit.Test.Common.Helpers; using Fido2NetLib; -using Fido2NetLib.Objects; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ReceivedExtensions; -using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Core.Test.Services; @@ -193,107 +185,6 @@ public class UserServiceTests Assert.True(await sutProvider.Sut.HasPremiumFromOrganization(user)); } - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginRegistrationAsync_ExceedsExistingCredentialsLimit_ReturnsFalse(SutProvider sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator credentialGenerator) - { - // Arrange - var existingCredentials = credentialGenerator.Take(5).ToList(); - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(existingCredentials); - - // Act - var result = await sutProvider.Sut.CompleteWebAuthLoginRegistrationAsync(user, "name", options, response, false, null, null, null); - - // Assert - Assert.False(result); - sutProvider.GetDependency().DidNotReceive(); - } - - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginAssertionAsync_InvalidUserHandle_ThrowsBadRequestException(SutProvider sutProvider, AssertionOptions options, AuthenticatorAssertionRawResponse response) - { - // Arrange - response.Response.UserHandle = Encoding.UTF8.GetBytes("invalid-user-handle"); - - // Act - var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response); - - // Assert - await Assert.ThrowsAsync(result); - } - - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginAssertionAsync_UserNotFound_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response) - { - // Arrange - response.Response.UserHandle = user.Id.ToByteArray(); - sutProvider.GetDependency().GetByIdAsync(user.Id).ReturnsNull(); - - // Act - var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response); - - // Assert - await Assert.ThrowsAsync(result); - } - - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginAssertionAsync_NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response) - { - // Arrange - response.Response.UserHandle = user.Id.ToByteArray(); - sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { }); - - // Act - var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response); - - // Assert - await Assert.ThrowsAsync(result); - } - - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginAssertionAsync_AssertionFails_ThrowsBadRequestException(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult) - { - // Arrange - var credentialId = Guid.NewGuid().ToByteArray(); - credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId); - response.Id = credentialId; - response.Response.UserHandle = user.Id.ToByteArray(); - assertionResult.Status = "Not ok"; - sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential }); - sutProvider.GetDependency().MakeAssertionAsync(response, options, Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(assertionResult); - - // Act - var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response); - - // Assert - await Assert.ThrowsAsync(result); - } - - [Theory, BitAutoData] - public async Task CompleteWebAuthLoginAssertionAsync_AssertionSucceeds_ReturnsUserAndCredential(SutProvider sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult) - { - // Arrange - var credentialId = Guid.NewGuid().ToByteArray(); - credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId); - response.Id = credentialId; - response.Response.UserHandle = user.Id.ToByteArray(); - assertionResult.Status = "ok"; - sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential }); - sutProvider.GetDependency().MakeAssertionAsync(response, options, Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(assertionResult); - - // Act - var result = await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response); - - // Assert - var (userResult, credentialResult) = result; - Assert.Equal(user, userResult); - Assert.Equal(credential, credentialResult); - } - [Flags] public enum ShouldCheck { @@ -369,8 +260,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - new FakeDataProtectorTokenFactory(), - sutProvider.GetDependency() + new FakeDataProtectorTokenFactory() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index 4690583f7..a46bf3867 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Services; +using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -26,6 +27,7 @@ public class AccountsControllerTests : IDisposable private readonly IUserService _userService; private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; + private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; public AccountsControllerTests() { @@ -34,12 +36,14 @@ public class AccountsControllerTests : IDisposable _userService = Substitute.For(); _captchaValidationService = Substitute.For(); _assertionOptionsDataProtector = Substitute.For>(); + _getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For(); _sut = new AccountsController( _logger, _userRepository, _userService, _captchaValidationService, - _assertionOptionsDataProtector + _assertionOptionsDataProtector, + _getWebAuthnLoginCredentialAssertionOptionsCommand ); }