mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 14:13:18 +08: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:
@@ -1,14 +1,17 @@
|
||||
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;
|
||||
@@ -33,7 +36,9 @@ public class AccountsControllerTests : IDisposable
|
||||
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;
|
||||
@@ -49,7 +54,9 @@ public class AccountsControllerTests : IDisposable
|
||||
_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>();
|
||||
@@ -64,6 +71,8 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService,
|
||||
_policyService,
|
||||
_setInitialMasterPasswordCommand,
|
||||
_setInitialMasterPasswordCommandV1,
|
||||
_tdeSetPasswordCommand,
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_featureService,
|
||||
@@ -379,13 +388,13 @@ public class AccountsControllerTests : IDisposable
|
||||
[BitAutoData(true, null, "newPublicKey", false)]
|
||||
// reject overwriting existing keys
|
||||
[BitAutoData(true, "newPrivateKey", "newPublicKey", false)]
|
||||
public async Task PostSetPasswordAsync_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
|
||||
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
|
||||
bool hasExistingKeys,
|
||||
string requestPrivateKey,
|
||||
string requestPublicKey,
|
||||
bool shouldSucceed,
|
||||
User user,
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existingPublicKey";
|
||||
@@ -402,13 +411,15 @@ public class AccountsControllerTests : IDisposable
|
||||
user.PrivateKey = null;
|
||||
}
|
||||
|
||||
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
|
||||
|
||||
if (requestPrivateKey == null && requestPublicKey == null)
|
||||
{
|
||||
setPasswordRequestModel.Keys = null;
|
||||
setInitialPasswordRequestModel.Keys = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
setPasswordRequestModel.Keys = new KeysRequestModel
|
||||
setInitialPasswordRequestModel.Keys = new KeysRequestModel
|
||||
{
|
||||
EncryptedPrivateKey = requestPrivateKey,
|
||||
PublicKey = requestPublicKey
|
||||
@@ -416,44 +427,44 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
|
||||
user,
|
||||
setPasswordRequestModel.MasterPasswordHash,
|
||||
setPasswordRequestModel.Key,
|
||||
setPasswordRequestModel.OrgIdentifier)
|
||||
setInitialPasswordRequestModel.MasterPasswordHash,
|
||||
setInitialPasswordRequestModel.Key,
|
||||
setInitialPasswordRequestModel.OrgIdentifier)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
if (shouldSucceed)
|
||||
{
|
||||
await _sut.PostSetPasswordAsync(setPasswordRequestModel);
|
||||
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
|
||||
// Assert
|
||||
await _setInitialMasterPasswordCommand.Received(1)
|
||||
await _setInitialMasterPasswordCommandV1.Received(1)
|
||||
.SetInitialMasterPasswordAsync(
|
||||
Arg.Is<User>(u => u == user),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.MasterPasswordHash),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.Key),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.OrgIdentifier));
|
||||
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(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
|
||||
Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf);
|
||||
Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(setPasswordRequestModel.Key, user.Key);
|
||||
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(setPasswordRequestModel));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetPasswordAsync_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(
|
||||
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(
|
||||
User user,
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existingPublicKey";
|
||||
@@ -465,47 +476,52 @@ public class AccountsControllerTests : IDisposable
|
||||
user.PublicKey = existingPublicKey;
|
||||
user.PrivateKey = existingEncryptedPrivateKey;
|
||||
|
||||
setPasswordRequestModel.Keys = new KeysRequestModel()
|
||||
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
|
||||
|
||||
setInitialPasswordRequestModel.Keys = new KeysRequestModel()
|
||||
{
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
};
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
|
||||
user,
|
||||
setPasswordRequestModel.MasterPasswordHash,
|
||||
setPasswordRequestModel.Key,
|
||||
setPasswordRequestModel.OrgIdentifier)
|
||||
setInitialPasswordRequestModel.MasterPasswordHash,
|
||||
setInitialPasswordRequestModel.Key,
|
||||
setInitialPasswordRequestModel.OrgIdentifier)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setPasswordRequestModel));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetPasswordAsync_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
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(setPasswordRequestModel));
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetPasswordAsync_WhenSettingPasswordFails_ShouldThrowBadRequestException(
|
||||
public async Task PostSetPasswordAsync_V1_WhenSettingPasswordFails_ShouldThrowBadRequestException(
|
||||
User user,
|
||||
SetPasswordRequestModel model)
|
||||
SetInitialPasswordRequestModel model)
|
||||
{
|
||||
UpdateSetInitialPasswordRequestModelToV1(model);
|
||||
model.Keys = null;
|
||||
// Arrange
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
_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
|
||||
@@ -845,5 +861,139 @@ public class AccountsControllerTests : IDisposable
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user