1
0
mirror of https://github.com/bitwarden/server synced 2026-01-15 23:13:56 +00:00
Files
server/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
Maciej Zieniuk 2e92a53f11 [PM-27281] Support v2 account encryption on JIT master password signups (#6777)
* V2 prep, rename existing SSO JIT MP command to V1

* set initial master password for account registraton V2

* later removel docs

* TDE MP onboarding split

* revert separate TDE onboarding controller api

* Server side hash of the user master password hash

* use `ValidationResult` instead for validation errors

* unit test coverage

* integration test coverage

* update sql migration script date

* revert validate password change

* better requests validation

* explicit error message when org sso identifier invalid

* more unit test coverage

* renamed onboarding to set, hash naming clarifications

* update db sql script, formatting

* use raw json as request instead of request models for integration test

* v1 integration test coverage

* change of name
2026-01-09 09:17:45 +01:00

1000 lines
38 KiB
C#

using System.Security.Claims;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Controllers;
public class AccountsControllerTests : IDisposable
{
private readonly AccountsController _sut;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
private readonly IUserRepository _userRepository;
public AccountsControllerTests()
{
_userService = Substitute.For<IUserService>();
_organizationService = Substitute.For<IOrganizationService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_providerUserRepository = Substitute.For<IProviderUserRepository>();
_policyService = Substitute.For<IPolicyService>();
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
_setInitialMasterPasswordCommandV1 = Substitute.For<ISetInitialMasterPasswordCommandV1>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeSetPasswordCommand = Substitute.For<ITdeSetPasswordCommand>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_userRepository = Substitute.For<IUserRepository>();
_sut = new AccountsController(
_organizationService,
_organizationUserRepository,
_providerUserRepository,
_userService,
_policyService,
_setInitialMasterPasswordCommand,
_setInitialMasterPasswordCommandV1,
_tdeSetPasswordCommand,
_tdeOffboardingPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService,
_userAccountKeysQuery,
_twoFactorEmailService,
_changeKdfCommand,
_userRepository
);
}
public void Dispose()
{
_sut?.Dispose();
}
[Fact]
public async Task PostPasswordHint_ShouldNotifyUserService()
{
var email = "user@example.com";
await _sut.PostPasswordHint(new PasswordHintRequestModel { Email = email });
await _userService.Received(1).SendMasterPasswordHintAsync(email);
}
[Fact]
public async Task PostEmailToken_ShouldInitiateEmailChange()
{
// Arrange
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
const string newEmail = "example@user.com";
_userService.ValidateClaimedUserDomainAsync(user, newEmail).Returns(IdentityResult.Success);
// Act
await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
// Assert
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
}
[Fact]
public async Task PostEmailToken_WhenValidateClaimedUserDomainAsyncFails_ShouldReturnError()
{
// Arrange
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
const string newEmail = "example@user.com";
_userService.ValidateClaimedUserDomainAsync(user, newEmail)
.Returns(IdentityResult.Failed(new IdentityError
{
Code = "TestFailure",
Description = "This is a test."
}));
// Act
// Assert
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail })
);
}
[Fact]
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel())
);
}
[Fact]
public async Task PostEmailToken_WhenInvalidPasssword_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel())
);
}
[Fact]
public async Task PostEmail_ShouldChangeUserEmail()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangeEmailAsync(user, default, default, default, default, default)
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostEmail(new EmailRequestModel());
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
}
[Fact]
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostEmail(new EmailRequestModel())
);
}
[Fact]
public async Task PostEmail_WhenEmailCannotBeChanged_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangeEmailAsync(user, default, default, default, default, default)
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmail(new EmailRequestModel())
);
}
[Fact]
public async Task PostVerifyEmail_ShouldSendEmailVerification()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
await _sut.PostVerifyEmail();
await _userService.Received(1).SendEmailVerificationAsync(user);
}
[Fact]
public async Task PostVerifyEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostVerifyEmail()
);
}
[Fact]
public async Task PostVerifyEmailToken_ShouldConfirmEmail()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.ConfirmEmailAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" });
await _userService.Received(1).ConfirmEmailAsync(user, Arg.Any<string>());
}
[Fact]
public async Task PostVerifyEmailToken_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnNullUserId();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
);
}
[Fact]
public async Task PostVerifyEmailToken_WhenEmailConfirmationFails_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.ConfirmEmailAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
);
}
[Fact]
public async Task PostPassword_ShouldChangePassword()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
});
await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
public async Task PostPassword_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
[Fact]
public async Task PostPassword_WhenPasswordChangeFails_ShouldBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
[Fact]
public async Task GetApiKey_ShouldReturnApiKeyResponse()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
await _sut.ApiKey(new SecretVerificationRequestModel());
}
[Fact]
public async Task GetApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
[Fact]
public async Task GetApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
[Fact]
public async Task PostRotateApiKey_ShouldRotateApiKey()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
await _sut.RotateApiKey(new SecretVerificationRequestModel());
}
[Fact]
public async Task PostRotateApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
[Fact]
public async Task PostRotateApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
[Theory]
[BitAutoData(true, "existingPrivateKey", "existingPublicKey", true)] // allow providing existing keys in the request
[BitAutoData(true, null, null, true)] // allow not setting the public key when the user already has a key
[BitAutoData(false, "newPrivateKey", "newPublicKey", true)] // allow setting new keys when the user has no keys
[BitAutoData(false, null, null, true)] // allow not setting the public key when the user has no keys
// do not allow single key
[BitAutoData(false, "existingPrivateKey", null, false)]
[BitAutoData(false, null, "existingPublicKey", false)]
[BitAutoData(false, "newPrivateKey", null, false)]
[BitAutoData(false, null, "newPublicKey", false)]
[BitAutoData(true, "existingPrivateKey", null, false)]
[BitAutoData(true, null, "existingPublicKey", false)]
[BitAutoData(true, "newPrivateKey", null, false)]
[BitAutoData(true, null, "newPublicKey", false)]
// reject overwriting existing keys
[BitAutoData(true, "newPrivateKey", "newPublicKey", false)]
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
bool hasExistingKeys,
string requestPrivateKey,
string requestPublicKey,
bool shouldSucceed,
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
const string existingPublicKey = "existingPublicKey";
const string existingEncryptedPrivateKey = "existingPrivateKey";
if (hasExistingKeys)
{
user.PublicKey = existingPublicKey;
user.PrivateKey = existingEncryptedPrivateKey;
}
else
{
user.PublicKey = null;
user.PrivateKey = null;
}
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
if (requestPrivateKey == null && requestPublicKey == null)
{
setInitialPasswordRequestModel.Keys = null;
}
else
{
setInitialPasswordRequestModel.Keys = new KeysRequestModel
{
EncryptedPrivateKey = requestPrivateKey,
PublicKey = requestPublicKey
};
}
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
setInitialPasswordRequestModel.MasterPasswordHash,
setInitialPasswordRequestModel.Key,
setInitialPasswordRequestModel.OrgIdentifier)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
if (shouldSucceed)
{
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _setInitialMasterPasswordCommandV1.Received(1)
.SetInitialMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.MasterPasswordHash),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.Key),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.OrgIdentifier));
// Additional Assertions for User object modifications
Assert.Equal(setInitialPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
Assert.Equal(setInitialPasswordRequestModel.Kdf, user.Kdf);
Assert.Equal(setInitialPasswordRequestModel.KdfIterations, user.KdfIterations);
Assert.Equal(setInitialPasswordRequestModel.KdfMemory, user.KdfMemory);
Assert.Equal(setInitialPasswordRequestModel.KdfParallelism, user.KdfParallelism);
Assert.Equal(setInitialPasswordRequestModel.Key, user.Key);
}
else
{
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
const string existingPublicKey = "existingPublicKey";
const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey";
const string newPublicKey = "newPublicKey";
const string newEncryptedPrivateKey = "newEncryptedPrivateKey";
user.PublicKey = existingPublicKey;
user.PrivateKey = existingEncryptedPrivateKey;
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
setInitialPasswordRequestModel.Keys = new KeysRequestModel()
{
PublicKey = newPublicKey,
EncryptedPrivateKey = newEncryptedPrivateKey
};
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
setInitialPasswordRequestModel.MasterPasswordHash,
setInitialPasswordRequestModel.Key,
setInitialPasswordRequestModel.OrgIdentifier)
.Returns(Task.FromResult(IdentityResult.Success));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenSettingPasswordFails_ShouldThrowBadRequestException(
User user,
SetInitialPasswordRequestModel model)
{
UpdateSetInitialPasswordRequestModelToV1(model);
model.Keys = null;
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Some Error" })));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
}
[Fact]
public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
}
[Fact]
public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
await _sut.Delete(new SecretVerificationRequestModel());
await _userService.Received(1).DeleteAsync(user);
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetVerifyDevicesRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.SetUserVerifyDevicesAsync(model));
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenInvalidSecret_ShouldFail(
User user, SetVerifyDevicesRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.SetUserVerifyDevicesAsync(model));
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenRequestValid_ShouldSucceed(
User user, SetVerifyDevicesRequestModel model)
{
// Arrange
user.VerifyDevices = false;
model.VerifyDevices = true;
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.SetUserVerifyDevicesAsync(model);
await _userService.Received(1).SaveUserAsync(user);
Assert.Equal(model.VerifyDevices, user.VerifyDevices);
}
[Theory]
[BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail(
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail(
User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.ResendNewDeviceOtpAsync(model);
// Assert
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
}
[Theory]
[BitAutoData]
public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
// Act
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKdf(model));
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.AuthenticationData = null;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullUnlockData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.UnlockData = null;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task PostKdf_ChangeKdfFailed_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" })));
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.NotNull(exception.ModelState);
Assert.Contains("Change KDF failed",
exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));
}
[Theory]
[BitAutoData]
public async Task PostKdf_ChangeKdfSuccess_NoError(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
.Returns(Task.FromResult(IdentityResult.Success));
// Act
await _sut.PostKdf(model);
}
[Theory]
[BitAutoData]
public async Task PostKeys_NoUser_Errors(KeysRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKeys(model));
}
[Theory]
[BitAutoData("existing", "existing")]
[BitAutoData((string)null, "existing")]
[BitAutoData("", "existing")]
[BitAutoData(" ", "existing")]
[BitAutoData("existing", null)]
[BitAutoData("existing", "")]
[BitAutoData("existing", " ")]
public async Task PostKeys_UserAlreadyHasKeys_Errors(string? existingPrivateKey, string? existingPublicKey,
KeysRequestModel model)
{
var user = GenerateExampleUser();
user.PrivateKey = existingPrivateKey;
user.PublicKey = existingPublicKey;
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKeys(model));
Assert.NotNull(exception.Message);
Assert.Contains("User has existing keypair", exception.Message);
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with
// other test suites. They are included here for the time being
// until that day comes.
private User GenerateExampleUser()
{
return new User
{
Email = "user@example.com"
};
}
private void ConfigureUserServiceToReturnNullPrincipal()
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(Task.FromResult((User)null));
}
private void ConfigureUserServiceToReturnValidPrincipalFor(User user)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(Task.FromResult(user));
}
private void ConfigureUserServiceToRejectPasswordFor(User user)
{
_userService.CheckPasswordAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(false));
}
private void ConfigureUserServiceToAcceptPasswordFor(User user)
{
_userService.CheckPasswordAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
_userService.VerifySecretAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
}
private void ConfigureUserServiceToReturnValidIdFor(User user)
{
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.Returns(Task.FromResult(user));
}
private void ConfigureUserServiceToReturnNullUserId()
{
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.Returns(Task.FromResult((User)null));
}
[Theory, BitAutoData]
public async Task PostKeys_WithAccountKeys_CallsSetV2AccountCryptographicState(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = null;
user.PrivateKey = null;
model.AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "wrapped-private-key",
AccountPublicKey = "public-key",
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "public-key",
WrappedPrivateKey = "wrapped-private-key",
SignedPublicKey = "signed-public-key"
},
SignatureKeyPair = new SignatureKeyPairRequestModel
{
VerifyingKey = "verifying-key",
SignatureAlgorithm = "ed25519",
WrappedSigningKey = "wrapped-signing-key"
},
SecurityState = new SecurityStateModel
{
SecurityState = "security-state",
SecurityVersion = 2
}
};
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userRepository.Received(1).SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Any<UserAccountKeysData>());
await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any<User>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
[Theory, BitAutoData]
public async Task PostKeys_WithoutAccountKeys_CallsSaveUser(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = null;
user.PrivateKey = null;
model.AccountKeys = null;
model.PublicKey = "public-key";
model.EncryptedPrivateKey = "encrypted-private-key";
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.PublicKey == model.PublicKey &&
u.PrivateKey == model.EncryptedPrivateKey));
await _userRepository.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenUserExistsAndSettingPasswordSucceeds_ShouldSetInitialMasterPassword(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.CompletedTask);
// Act
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _setInitialMasterPasswordCommand.Received(1)
.SetInitialMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<SetInitialMasterPasswordDataModel>(d =>
d.MasterPasswordAuthentication != null &&
d.MasterPasswordUnlock != null &&
d.AccountKeys != null &&
d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WithTdeSetPassword_ShouldCallTdeSetPasswordCommand(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel, includeTdeSetPassword: true);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_tdeSetPasswordCommand.SetMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.CompletedTask);
// Act
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _tdeSetPasswordCommand.Received(1)
.SetMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<SetInitialMasterPasswordDataModel>(d =>
d.MasterPasswordAuthentication != null &&
d.MasterPasswordUnlock != null &&
d.AccountKeys == null &&
d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenSettingPasswordFails_ShouldThrowException(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.FromException(new Exception("Setting password failed")));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
private void UpdateSetInitialPasswordRequestModelToV1(SetInitialPasswordRequestModel model)
{
model.MasterPasswordAuthentication = null;
model.MasterPasswordUnlock = null;
model.AccountKeys = null;
}
private void UpdateSetInitialPasswordRequestModelToV2(SetInitialPasswordRequestModel model, bool includeTdeSetPassword = false)
{
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
};
model.MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
};
model.MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
};
if (includeTdeSetPassword)
{
// TDE set password does not include AccountKeys
model.AccountKeys = null;
}
else
{
model.AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "privateKey",
AccountPublicKey = "publicKey"
};
}
// Clear V1 properties
model.MasterPasswordHash = null;
model.Key = null;
model.Keys = null;
model.Kdf = null;
model.KdfIterations = null;
model.KdfMemory = null;
model.KdfParallelism = null;
}
}