1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 09:02:48 -05: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:
Andreas Coroiu
2023-12-14 09:35:52 +01:00
committed by GitHub
parent 27d7d823a7
commit d63c917c95
20 changed files with 500 additions and 245 deletions

View File

@ -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>();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
using Fido2NetLib;
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
public interface IGetWebAuthnLoginCredentialAssertionOptionsCommand
{
public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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)