1
0
mirror of https://github.com/bitwarden/server synced 2026-01-19 08:53:57 +00:00

[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
This commit is contained in:
Maciej Zieniuk
2026-01-09 09:17:45 +01:00
committed by GitHub
parent 62ae828143
commit 2e92a53f11
25 changed files with 2642 additions and 279 deletions

View File

@@ -1,8 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -21,106 +23,154 @@ public class SetInitialMasterPasswordCommandTests
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_Success(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier,
Organization org, OrganizationUser orgUser)
User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings,
Organization org, OrganizationUser orgUser, string serverSideHash, string masterPasswordHint)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
user.Key = null;
var model = CreateValidModel(user, accountKeys, kdfSettings, org.Identifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
sutProvider.GetDependency<IPasswordHasher<User>>()
.HashPassword(user, model.MasterPasswordAuthentication.MasterPasswordAuthenticationHash)
.Returns(serverSideHash);
// Assert
Assert.Equal(IdentityResult.Success, result);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserIsNull_ThrowsArgumentNullException(SutProvider<SetInitialMasterPasswordCommand> sutProvider, string masterPassword, string key, string orgIdentifier)
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(null, masterPassword, key, orgIdentifier));
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_AlreadyHasPassword_ReturnsFalse(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
{
// Arrange
user.MasterPassword = "ExistingPassword";
// Mock SetMasterPassword to return a specific UpdateUserData delegate
UpdateUserData mockUpdateUserData = (connection, transaction) => Task.CompletedTask;
sutProvider.GetDependency<IUserRepository>()
.SetMasterPassword(user.Id, model.MasterPasswordUnlock, serverSideHash, model.MasterPasswordHint)
.Returns(mockUpdateUserData);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model);
// Assert
Assert.False(result.Succeeded);
await sutProvider.GetDependency<IUserRepository>().Received(1)
.SetV2AccountCryptographicStateAsync(
user.Id,
model.AccountKeys,
Arg.Do<IEnumerable<UpdateUserData>>(actions =>
{
var actionsList = actions.ToList();
Assert.Single(actionsList);
Assert.Same(mockUpdateUserData, actionsList[0]);
}));
await sutProvider.GetDependency<IEventService>().Received(1)
.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_NullOrgSsoIdentifier_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key)
public async Task SetInitialMasterPassword_UserAlreadyHasPassword_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.MasterPassword = null;
string orgSsoIdentifier = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
user.Key = "existing-key";
var model = CreateValidModel(user, accountKeys, kdfSettings, orgSsoIdentifier, masterPasswordHint);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgSsoIdentifier));
Assert.Equal("Organization SSO Identifier required.", exception.Message);
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));
Assert.Equal("User already has a master password set.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvalidOrganization_Throws(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
public async Task SetInitialMasterPassword_AccountKeysNull_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.MasterPassword = null;
user.Key = null;
var model = CreateValidModel(user, null, kdfSettings, orgSsoIdentifier, masterPasswordHint);
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));
Assert.Equal("Account keys are required.", exception.Message);
}
[Theory]
[BitAutoData("wrong-salt", null)]
[BitAutoData([null, "wrong-salt"])]
[BitAutoData("wrong-salt", "different-wrong-salt")]
public async Task SetInitialMasterPassword_InvalidSalt_ThrowsBadRequestException(
string? authSaltOverride, string? unlockSaltOverride,
SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = null;
var correctSalt = user.GetMasterPasswordSalt();
var model = new SetInitialMasterPasswordDataModel
{
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
{
Salt = authSaltOverride ?? correctSalt,
MasterPasswordAuthenticationHash = "hash",
Kdf = kdfSettings
},
MasterPasswordUnlock = new MasterPasswordUnlockData
{
Salt = unlockSaltOverride ?? correctSalt,
MasterKeyWrappedUserKey = "wrapped-key",
Kdf = kdfSettings
},
AccountKeys = accountKeys,
OrgSsoIdentifier = orgSsoIdentifier,
MasterPasswordHint = masterPasswordHint
};
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));
Assert.Equal("Invalid master password salt.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvalidOrgSsoIdentifier_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = null;
var model = CreateValidModel(user, accountKeys, kdfSettings, orgSsoIdentifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.GetByIdentifierAsync(orgSsoIdentifier)
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier));
Assert.Equal("Organization invalid.", exception.Message);
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));
Assert.Equal("Organization SSO identifier is invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserNotFoundInOrganization_Throws(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, Organization org)
public async Task SetInitialMasterPassword_UserNotFoundInOrganization_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, Organization org, string masterPasswordHint)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
user.Key = null;
var model = CreateValidModel(user, accountKeys, kdfSettings, org.Identifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(Arg.Any<string>())
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
@@ -128,67 +178,33 @@ public class SetInitialMasterPasswordCommandTests
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, org.Identifier));
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));
Assert.Equal("User not found within organization.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_ConfirmedOrgUser_DoesNotCallAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
private static SetInitialMasterPasswordDataModel CreateValidModel(
User user, UserAccountKeysData? accountKeys, KdfSettings kdfSettings,
string orgSsoIdentifier, string? masterPasswordHint)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().DidNotReceive().AcceptOrgUserAsync(Arg.Any<OrganizationUser>(), Arg.Any<User>(), Arg.Any<IUserService>());
var salt = user.GetMasterPasswordSalt();
return new SetInitialMasterPasswordDataModel
{
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
{
Salt = salt,
MasterPasswordAuthenticationHash = "hash",
Kdf = kdfSettings
},
MasterPasswordUnlock = new MasterPasswordUnlockData
{
Salt = salt,
MasterKeyWrappedUserKey = "wrapped-key",
Kdf = kdfSettings
},
AccountKeys = accountKeys,
OrgSsoIdentifier = orgSsoIdentifier,
MasterPasswordHint = masterPasswordHint
};
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvitedOrgUser_CallsAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());
}
}

View File

@@ -0,0 +1,194 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;
[SutProviderCustomize]
public class SetInitialMasterPasswordCommandV1Tests
{
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_Success(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,
User user, string masterPassword, string key, string orgIdentifier,
Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserIsNull_ThrowsArgumentNullException(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, string masterPassword, string key, string orgIdentifier)
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(null, masterPassword, key, orgIdentifier));
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_AlreadyHasPassword_ReturnsFalse(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
{
// Arrange
user.MasterPassword = "ExistingPassword";
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.False(result.Succeeded);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_NullOrgSsoIdentifier_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key)
{
// Arrange
user.MasterPassword = null;
string orgSsoIdentifier = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgSsoIdentifier));
Assert.Equal("Organization SSO Identifier required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvalidOrganization_Throws(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier));
Assert.Equal("Organization invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserNotFoundInOrganization_Throws(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, Organization org)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(Arg.Any<string>())
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, org.Identifier));
Assert.Equal("User not found within organization.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_ConfirmedOrgUser_DoesNotCallAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().DidNotReceive().AcceptOrgUserAsync(Arg.Any<OrganizationUser>(), Arg.Any<User>(), Arg.Any<IUserService>());
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvitedOrgUser_CallsAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());
}
}

View File

@@ -0,0 +1,223 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;
[SutProviderCustomize]
public class TdeSetPasswordCommandTests
{
[Theory]
[BitAutoData]
public async Task OnboardMasterPassword_Success(SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings,
Organization org, OrganizationUser orgUser, string serverSideHash, string masterPasswordHint)
{
// Arrange
user.Key = null;
user.PublicKey = "public-key";
user.PrivateKey = "private-key";
var model = CreateValidModel(user, kdfSettings, org.Identifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
sutProvider.GetDependency<IPasswordHasher<User>>()
.HashPassword(user, model.MasterPasswordAuthentication.MasterPasswordAuthenticationHash)
.Returns(serverSideHash);
// Mock SetMasterPassword to return a specific UpdateUserData delegate
UpdateUserData mockUpdateUserData = (connection, transaction) => Task.CompletedTask;
sutProvider.GetDependency<IUserRepository>()
.SetMasterPassword(user.Id, model.MasterPasswordUnlock, serverSideHash, model.MasterPasswordHint)
.Returns(mockUpdateUserData);
// Act
await sutProvider.Sut.SetMasterPasswordAsync(user, model);
// Assert
await sutProvider.GetDependency<IUserRepository>().Received(1)
.UpdateUserDataAsync(Arg.Do<IEnumerable<UpdateUserData>>(actions =>
{
var actionsList = actions.ToList();
Assert.Single(actionsList);
Assert.Same(mockUpdateUserData, actionsList[0]);
}));
await sutProvider.GetDependency<IEventService>().Received(1)
.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
}
[Theory]
[BitAutoData]
public async Task OnboardMasterPassword_UserAlreadyHasPassword_ThrowsBadRequestException(
SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = "existing-key";
var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);
// Act & Assert
var exception =
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetMasterPasswordAsync(user, model));
Assert.Equal("User already has a master password set.", exception.Message);
}
[Theory]
[BitAutoData([null, "private-key"])]
[BitAutoData("public-key", null)]
[BitAutoData([null, null])]
public async Task OnboardMasterPassword_MissingAccountKeys_ThrowsBadRequestException(
string? publicKey, string? privateKey,
SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = null;
user.PublicKey = publicKey;
user.PrivateKey = privateKey;
var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);
// Act & Assert
var exception =
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetMasterPasswordAsync(user, model));
Assert.Equal("TDE user account keys must be set before setting initial master password.", exception.Message);
}
[Theory]
[BitAutoData("wrong-salt", null)]
[BitAutoData([null, "wrong-salt"])]
[BitAutoData("wrong-salt", "different-wrong-salt")]
public async Task OnboardMasterPassword_InvalidSalt_ThrowsBadRequestException(
string? authSaltOverride, string? unlockSaltOverride,
SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = null;
user.PublicKey = "public-key";
user.PrivateKey = "private-key";
var correctSalt = user.GetMasterPasswordSalt();
var model = new SetInitialMasterPasswordDataModel
{
MasterPasswordAuthentication =
new MasterPasswordAuthenticationData
{
Salt = authSaltOverride ?? correctSalt,
MasterPasswordAuthenticationHash = "hash",
Kdf = kdfSettings
},
MasterPasswordUnlock = new MasterPasswordUnlockData
{
Salt = unlockSaltOverride ?? correctSalt,
MasterKeyWrappedUserKey = "wrapped-key",
Kdf = kdfSettings
},
AccountKeys = null,
OrgSsoIdentifier = orgSsoIdentifier,
MasterPasswordHint = masterPasswordHint
};
// Act & Assert
var exception =
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetMasterPasswordAsync(user, model));
Assert.Equal("Invalid master password salt.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task OnboardMasterPassword_InvalidOrgSsoIdentifier_ThrowsBadRequestException(
SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)
{
// Arrange
user.Key = null;
user.PublicKey = "public-key";
user.PrivateKey = "private-key";
var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgSsoIdentifier)
.ReturnsNull();
// Act & Assert
var exception =
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetMasterPasswordAsync(user, model));
Assert.Equal("Organization SSO identifier is invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task OnboardMasterPassword_UserNotFoundInOrganization_ThrowsBadRequestException(
SutProvider<TdeSetPasswordCommand> sutProvider,
User user, KdfSettings kdfSettings, Organization org, string masterPasswordHint)
{
// Arrange
user.Key = null;
user.PublicKey = "public-key";
user.PrivateKey = "private-key";
var model = CreateValidModel(user, kdfSettings, org.Identifier, masterPasswordHint);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.ReturnsNull();
// Act & Assert
var exception =
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetMasterPasswordAsync(user, model));
Assert.Equal("User not found within organization.", exception.Message);
}
private static SetInitialMasterPasswordDataModel CreateValidModel(
User user, KdfSettings kdfSettings, string orgSsoIdentifier, string? masterPasswordHint)
{
var salt = user.GetMasterPasswordSalt();
return new SetInitialMasterPasswordDataModel
{
MasterPasswordAuthentication =
new MasterPasswordAuthenticationData
{
Salt = salt,
MasterPasswordAuthenticationHash = "hash",
Kdf = kdfSettings
},
MasterPasswordUnlock =
new MasterPasswordUnlockData
{
Salt = salt,
MasterKeyWrappedUserKey = "wrapped-key",
Kdf = kdfSettings
},
AccountKeys = null,
OrgSsoIdentifier = orgSsoIdentifier,
MasterPasswordHint = masterPasswordHint
};
}
}