mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-4619] Rewrite UserService
methods as commands (#3432)
* [PM-4619] feat: scaffold new create options command * [PM-4169] feat: implement credential create options command * [PM-4619] feat: create command for credential creation * [PM-4619] feat: create assertion options command * [PM-4619] chore: clean-up unused argument * [PM-4619] feat: implement assertion command * [PM-4619] feat: migrate to commands * [PM-4619] fix: lint * [PM-4169] fix: use constant * [PM-4619] fix: lint I have no idea what this commit acutally changes, but the file seems to have some character encoding issues. This fix was generated by `dotnet format`
This commit is contained in:
parent
27d7d823a7
commit
d63c917c95
@ -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<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;
|
||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||
|
||||
public WebAuthnController(
|
||||
IUserService userService,
|
||||
IWebAuthnCredentialRepository credentialRepository,
|
||||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> 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.");
|
||||
|
@ -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<IUserService, UserService>();
|
||||
services.AddUserPasswordCommands();
|
||||
services.AddWebAuthnLoginCommands();
|
||||
}
|
||||
|
||||
private static void AddUserPasswordCommands(this IServiceCollection services)
|
||||
@ -21,4 +24,11 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddWebAuthnLoginCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IGetWebAuthnLoginCredentialCreateOptionsCommand, GetWebAuthnLoginCredentialCreateOptionsCommand>();
|
||||
services.AddScoped<ICreateWebAuthnLoginCredentialCommand, CreateWebAuthnLoginCredentialCommand>();
|
||||
services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||
services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
public interface ICreateWebAuthnLoginCredentialCommand
|
||||
{
|
||||
public Task<bool> CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
public interface IGetWebAuthnLoginCredentialAssertionOptionsCommand
|
||||
{
|
||||
public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions();
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
/// <summary>
|
||||
/// Get the options required to create a Passkey for login.
|
||||
/// </summary>
|
||||
public interface IGetWebAuthnLoginCredentialCreateOptionsCommand
|
||||
{
|
||||
public Task<CredentialCreateOptions> GetWebAuthnLoginCredentialCreateOptionsAsync(User user);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<bool> 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;
|
||||
}
|
||||
}
|
@ -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<PublicKeyCredentialDescriptor>(), UserVerificationRequirement.Required);
|
||||
}
|
||||
}
|
@ -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<CredentialCreateOptions> 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;
|
||||
}
|
||||
}
|
@ -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<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
|
||||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(User user);
|
||||
Task<bool> 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<IdentityResult> ConfirmEmailAsync(User user, string token);
|
||||
Task InitiateEmailChangeAsync(User user, string newEmail);
|
||||
|
@ -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<User>, IUserService, IDisposable
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IStripeSyncService _stripeSyncService;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@ -97,8 +93,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IStripeSyncService stripeSyncService,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IWebAuthnCredentialRepository webAuthnRepository)
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@ -136,7 +131,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_stripeSyncService = stripeSyncService;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_webAuthnCredentialRepository = webAuthnRepository;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@ -522,114 +516,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<CredentialCreateOptions> 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<bool> 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<PublicKeyCredentialDescriptor>(), 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)
|
||||
|
@ -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<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
public AccountsController(
|
||||
ILogger<AccountsController> logger,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector)
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> 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);
|
||||
|
@ -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<ExtensionGrantValidat
|
||||
public const string GrantType = "webauthn";
|
||||
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
|
||||
public WebAuthnGrantValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -48,7 +50,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> 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<ExtensionGrantValidat
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
}
|
||||
|
||||
string IExtensionGrantValidator.GrantType => "webauthn";
|
||||
@ -86,7 +90,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
return;
|
||||
}
|
||||
|
||||
var (user, credential) = await _userService.CompleteWebAuthLoginAssertionAsync(token.Options, deviceResponse);
|
||||
var (user, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(token.Options, deviceResponse);
|
||||
var validatorContext = new CustomValidatorRequestContext
|
||||
{
|
||||
User = user,
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Request.Webauthn;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
@ -116,8 +117,8 @@ public class WebAuthnControllerTests
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
@ -142,8 +143,8 @@ public class WebAuthnControllerTests
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), false)
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), false)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
|
@ -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<AssertWebAuthnLoginCredentialCommand> 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<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async void UserNotFound_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).ReturnsNull();
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async void NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { });
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async void AssertionFails_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> 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<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.Returns(assertionResult);
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async void AssertionSucceeds_ReturnsUserAndCredential(SutProvider<AssertWebAuthnLoginCredentialCommand> 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<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.Returns(assertionResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);
|
||||
|
||||
// Assert
|
||||
var (userResult, credentialResult) = result;
|
||||
Assert.Equal(user, userResult);
|
||||
Assert.Equal(credential, credentialResult);
|
||||
}
|
||||
}
|
@ -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<CreateWebAuthnLoginCredentialCommand> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)
|
||||
{
|
||||
// Arrange
|
||||
var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser).ToList();
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().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<IWebAuthnCredentialRepository>().DidNotReceive().CreateAsync(Arg.Any<WebAuthnCredential>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async void DoesNotExceedExistingCredentialsLimit_CreatesCredential(SutProvider<CreateWebAuthnLoginCredentialCommand> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)
|
||||
{
|
||||
// Arrange
|
||||
var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser - 1).ToList();
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(existingCredentials);
|
||||
sutProvider.GetDependency<IFido2>().MakeNewCredentialAsync(
|
||||
response, options, Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()
|
||||
).Returns(MakeCredentialResult());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CreateWebAuthnLoginCredentialAsync(user, "name", options, response, false, null, null, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
await sutProvider.GetDependency<IWebAuthnCredentialRepository>().Received().CreateAsync(Arg.Any<WebAuthnCredential>());
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
@ -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<GetWebAuthnLoginCredentialCreateOptionsCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
// Assert
|
||||
sutProvider.GetDependency<IFido2>()
|
||||
.Received()
|
||||
.RequestNewCredential(
|
||||
Arg.Any<Fido2User>(),
|
||||
Arg.Is<List<PublicKeyCredentialDescriptor>>(list => list.Count == 0),
|
||||
Arg.Any<AuthenticatorSelection>(),
|
||||
Arg.Any<AttestationConveyancePreference>(),
|
||||
Arg.Any<AuthenticationExtensionsClientInputs>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
internal async Task HasExistingCredential_ReturnsOptionsWithExcludedCredential(SutProvider<GetWebAuthnLoginCredentialCreateOptionsCommand> sutProvider, User user, WebAuthnCredential credential)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { credential });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
// Assert
|
||||
sutProvider.GetDependency<IFido2>()
|
||||
.Received()
|
||||
.RequestNewCredential(
|
||||
Arg.Any<Fido2User>(),
|
||||
Arg.Is<List<PublicKeyCredentialDescriptor>>(list => list.Count == 1),
|
||||
Arg.Any<AuthenticatorSelection>(),
|
||||
Arg.Any<AttestationConveyancePreference>(),
|
||||
Arg.Any<AuthenticationExtensionsClientInputs>());
|
||||
}
|
||||
}
|
@ -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<UserService> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)
|
||||
{
|
||||
// Arrange
|
||||
var existingCredentials = credentialGenerator.Take(5).ToList();
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().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<IWebAuthnCredentialRepository>().DidNotReceive();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_InvalidUserHandle_ThrowsBadRequestException(SutProvider<UserService> 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<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_UserNotFound_ThrowsBadRequestException(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).ReturnsNull();
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { });
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_AssertionFails_ThrowsBadRequestException(SutProvider<UserService> 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<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.Returns(assertionResult);
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_AssertionSucceeds_ReturnsUserAndCredential(SutProvider<UserService> 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<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.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<IAcceptOrgUserCommand>(),
|
||||
sutProvider.GetDependency<IProviderUserRepository>(),
|
||||
sutProvider.GetDependency<IStripeSyncService>(),
|
||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>()
|
||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>()
|
||||
);
|
||||
|
||||
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
||||
|
@ -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<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
@ -34,12 +36,14 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_captchaValidationService = Substitute.For<ICaptchaValidationService>();
|
||||
_assertionOptionsDataProtector = Substitute.For<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>();
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For<IGetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||
_sut = new AccountsController(
|
||||
_logger,
|
||||
_userRepository,
|
||||
_userService,
|
||||
_captchaValidationService,
|
||||
_assertionOptionsDataProtector
|
||||
_assertionOptionsDataProtector,
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user