mirror of
https://github.com/bitwarden/server
synced 2025-12-14 23:33:41 +00:00
Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response (#6093)
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.KeyManagement.Models.Response;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models.Api.Response;
|
namespace Bit.Core.Auth.Models.Api.Response;
|
||||||
|
|
||||||
public class UserDecryptionOptions : ResponseModel
|
public class UserDecryptionOptions : ResponseModel
|
||||||
@@ -14,8 +13,15 @@ public class UserDecryptionOptions : ResponseModel
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the current user has a master password that can be used to decrypt their vault.
|
/// Gets or sets whether the current user has a master password that can be used to decrypt their vault.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Use MasterPasswordUnlock instead. This will be removed in a future version.")]
|
||||||
public bool HasMasterPassword { get; set; }
|
public bool HasMasterPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the current user has master password unlock data available.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the WebAuthn PRF decryption keys.
|
/// Gets or sets the WebAuthn PRF decryption keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.KeyManagement.Models.Response;
|
||||||
|
|
||||||
|
public class MasterPasswordUnlockResponseModel
|
||||||
|
{
|
||||||
|
public required MasterPasswordUnlockKdfResponseModel Kdf { get; init; }
|
||||||
|
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; init; }
|
||||||
|
[StringLength(256)] public required string Salt { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MasterPasswordUnlockKdfResponseModel
|
||||||
|
{
|
||||||
|
public required KdfType KdfType { get; init; }
|
||||||
|
public required int Iterations { get; init; }
|
||||||
|
public int? Memory { get; init; }
|
||||||
|
public int? Parallelism { get; init; }
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using Bit.Core.Auth.Utilities;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.KeyManagement.Models.Response;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Utilities;
|
using Bit.Identity.Utilities;
|
||||||
@@ -25,7 +26,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
|
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
|
||||||
|
|
||||||
private UserDecryptionOptions _options = new UserDecryptionOptions();
|
private UserDecryptionOptions _options = new UserDecryptionOptions();
|
||||||
private User? _user;
|
private User _user = null!;
|
||||||
private SsoConfig? _ssoConfig;
|
private SsoConfig? _ssoConfig;
|
||||||
private Device? _device;
|
private Device? _device;
|
||||||
|
|
||||||
@@ -44,7 +45,6 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
|
|
||||||
public IUserDecryptionOptionsBuilder ForUser(User user)
|
public IUserDecryptionOptionsBuilder ForUser(User user)
|
||||||
{
|
{
|
||||||
_options.HasMasterPassword = user.HasMasterPassword();
|
|
||||||
_user = user;
|
_user = user;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
|
|
||||||
public async Task<UserDecryptionOptions> BuildAsync()
|
public async Task<UserDecryptionOptions> BuildAsync()
|
||||||
{
|
{
|
||||||
|
BuildMasterPasswordUnlock();
|
||||||
BuildKeyConnectorOptions();
|
BuildKeyConnectorOptions();
|
||||||
await BuildTrustedDeviceOptions();
|
await BuildTrustedDeviceOptions();
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };
|
var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };
|
||||||
var isTdeOffboarding = _user != null && !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
|
var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
|
||||||
if (!isTdeActive && !isTdeOffboarding)
|
if (!isTdeActive && !isTdeOffboarding)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -116,7 +117,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hasLoginApprovingDevice = false;
|
var hasLoginApprovingDevice = false;
|
||||||
if (_device != null && _user != null)
|
if (_device != null)
|
||||||
{
|
{
|
||||||
var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);
|
var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);
|
||||||
// Checks if the current user has any devices that are capable of approving login with device requests except for
|
// Checks if the current user has any devices that are capable of approving login with device requests except for
|
||||||
@@ -134,16 +135,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId);
|
hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasAdminApproval = false;
|
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
|
||||||
if (_user != null)
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
|
||||||
{
|
|
||||||
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
|
|
||||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
|
|
||||||
|
|
||||||
hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin);
|
hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin);
|
||||||
// They are only able to be approved by an admin if they have enrolled is reset password
|
// They are only able to be approved by an admin if they have enrolled is reset password
|
||||||
hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||||
}
|
|
||||||
|
|
||||||
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||||
hasAdminApproval,
|
hasAdminApproval,
|
||||||
@@ -153,4 +150,28 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedUserKey);
|
encryptedUserKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildMasterPasswordUnlock()
|
||||||
|
{
|
||||||
|
if (_user.HasMasterPassword())
|
||||||
|
{
|
||||||
|
_options.HasMasterPassword = true;
|
||||||
|
_options.MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
|
||||||
|
{
|
||||||
|
Kdf = new MasterPasswordUnlockKdfResponseModel
|
||||||
|
{
|
||||||
|
KdfType = _user.Kdf,
|
||||||
|
Iterations = _user.KdfIterations,
|
||||||
|
Memory = _user.KdfMemory,
|
||||||
|
Parallelism = _user.KdfParallelism
|
||||||
|
},
|
||||||
|
MasterKeyEncryptedUserKey = _user.Key!,
|
||||||
|
Salt = _user.Email.ToLowerInvariant()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_options.HasMasterPassword = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,15 @@ public class IdentityServerSsoTests
|
|||||||
public async Task Test_MasterPassword_DecryptionType()
|
public async Task Test_MasterPassword_DecryptionType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
using var responseBody = await RunSuccessTestAsync(MemberDecryptionType.MasterPassword);
|
User? expectedUser = null;
|
||||||
|
using var responseBody = await RunSuccessTestAsync(async factory =>
|
||||||
|
{
|
||||||
|
var database = factory.GetDatabaseContext();
|
||||||
|
|
||||||
|
expectedUser = await database.Users.SingleAsync(u => u.Email == TestEmail);
|
||||||
|
Assert.NotNull(expectedUser);
|
||||||
|
}, MemberDecryptionType.MasterPassword);
|
||||||
|
Assert.NotNull(expectedUser);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// If the organization has a member decryption type of MasterPassword that should be the only option in the reply
|
// If the organization has a member decryption type of MasterPassword that should be the only option in the reply
|
||||||
@@ -47,13 +55,33 @@ public class IdentityServerSsoTests
|
|||||||
// Expected to look like:
|
// Expected to look like:
|
||||||
// "UserDecryptionOptions": {
|
// "UserDecryptionOptions": {
|
||||||
// "Object": "userDecryptionOptions"
|
// "Object": "userDecryptionOptions"
|
||||||
// "HasMasterPassword": true
|
// "HasMasterPassword": true,
|
||||||
|
// "MasterPasswordUnlock": {
|
||||||
|
// "Kdf": {
|
||||||
|
// "KdfType": 0,
|
||||||
|
// "Iterations": 600000
|
||||||
|
// },
|
||||||
|
// "MasterKeyEncryptedUserKey": "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==",
|
||||||
|
// "Salt": "sso_user@email.com"
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
||||||
|
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
|
||||||
// One property for the Object and one for master password
|
Assert.Equal("userDecryptionOptions", objectString);
|
||||||
Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count());
|
var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object);
|
||||||
|
// MasterPasswordUnlock.Kdf
|
||||||
|
var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object);
|
||||||
|
var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32();
|
||||||
|
Assert.Equal((int)expectedUser.Kdf, kdfType);
|
||||||
|
var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32();
|
||||||
|
Assert.Equal(expectedUser.KdfIterations, kdfIterations);
|
||||||
|
// MasterPasswordUnlock.MasterKeyEncryptedUserKey
|
||||||
|
var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString();
|
||||||
|
Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey);
|
||||||
|
// MasterPasswordUnlock.Salt
|
||||||
|
var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString();
|
||||||
|
Assert.Equal(TestEmail, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
Assert.Equal(0, kdf);
|
Assert.Equal(0, kdf);
|
||||||
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
|
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
|
||||||
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, kdfIterations);
|
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, kdfIterations);
|
||||||
AssertUserDecryptionOptions(root);
|
AssertUserDecryptionOptions(root, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, RegisterFinishRequestModelCustomize]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
@@ -601,14 +601,27 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant());
|
Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertUserDecryptionOptions(JsonElement tokenResponse)
|
private static void AssertUserDecryptionOptions(JsonElement tokenResponse, User expectedUser)
|
||||||
{
|
{
|
||||||
var userDecryptionOptions = AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object)
|
var userDecryptionOptions =
|
||||||
.EnumerateObject();
|
AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object);
|
||||||
|
|
||||||
Assert.Collection(userDecryptionOptions,
|
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
||||||
(prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); },
|
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
|
||||||
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
|
Assert.Equal("userDecryptionOptions", objectString);
|
||||||
|
var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object);
|
||||||
|
// MasterPasswordUnlock.Kdf
|
||||||
|
var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object);
|
||||||
|
var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32();
|
||||||
|
Assert.Equal((int)expectedUser.Kdf, kdfType);
|
||||||
|
var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32();
|
||||||
|
Assert.Equal(expectedUser.KdfIterations, kdfIterations);
|
||||||
|
// MasterPasswordUnlock.MasterKeyEncryptedUserKey
|
||||||
|
var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString();
|
||||||
|
Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey);
|
||||||
|
// MasterPasswordUnlock.Salt
|
||||||
|
var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString();
|
||||||
|
Assert.Equal(expectedUser.Email.ToLower(), salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReinitializeDbForTests(IdentityApplicationFactory factory)
|
private void ReinitializeDbForTests(IdentityApplicationFactory factory)
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Models.Api.Response;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.KeyManagement.Models.Response;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -27,6 +30,9 @@ namespace Bit.Identity.Test.IdentityServer;
|
|||||||
|
|
||||||
public class BaseRequestValidatorTests
|
public class BaseRequestValidatorTests
|
||||||
{
|
{
|
||||||
|
private static readonly string _mockEncryptedString =
|
||||||
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||||
|
|
||||||
private UserManager<User> _userManager;
|
private UserManager<User> _userManager;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
@@ -377,6 +383,102 @@ public class BaseRequestValidatorTests
|
|||||||
Assert.Equal(expectedMessage, errorResponse.Message);
|
Assert.Equal(expectedMessage, errorResponse.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ValidateAsync_CustomResponse_NoMasterPassword_ShouldSetUserDecryptionOptions(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
|
||||||
|
{
|
||||||
|
HasMasterPassword = false,
|
||||||
|
MasterPasswordUnlock = null
|
||||||
|
}));
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||||
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.GrantResult.IsError);
|
||||||
|
var customResponse = context.GrantResult.CustomResponse;
|
||||||
|
Assert.Contains("UserDecryptionOptions", customResponse);
|
||||||
|
Assert.IsType<UserDecryptionOptions>(customResponse["UserDecryptionOptions"]);
|
||||||
|
var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"];
|
||||||
|
Assert.False(userDecryptionOptions.HasMasterPassword);
|
||||||
|
Assert.Null(userDecryptionOptions.MasterPasswordUnlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
|
||||||
|
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
|
||||||
|
public async Task ValidateAsync_CustomResponse_MasterPassword_ShouldSetUserDecryptionOptions(
|
||||||
|
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
|
||||||
|
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
|
||||||
|
{
|
||||||
|
HasMasterPassword = true,
|
||||||
|
MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
|
||||||
|
{
|
||||||
|
Kdf = new MasterPasswordUnlockKdfResponseModel
|
||||||
|
{
|
||||||
|
KdfType = kdfType,
|
||||||
|
Iterations = kdfIterations,
|
||||||
|
Memory = kdfMemory,
|
||||||
|
Parallelism = kdfParallelism
|
||||||
|
},
|
||||||
|
MasterKeyEncryptedUserKey = _mockEncryptedString,
|
||||||
|
Salt = "test@example.com"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||||
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.GrantResult.IsError);
|
||||||
|
var customResponse = context.GrantResult.CustomResponse;
|
||||||
|
Assert.Contains("UserDecryptionOptions", customResponse);
|
||||||
|
Assert.IsType<UserDecryptionOptions>(customResponse["UserDecryptionOptions"]);
|
||||||
|
var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"];
|
||||||
|
Assert.True(userDecryptionOptions.HasMasterPassword);
|
||||||
|
Assert.NotNull(userDecryptionOptions.MasterPasswordUnlock);
|
||||||
|
Assert.Equal(kdfType, userDecryptionOptions.MasterPasswordUnlock.Kdf.KdfType);
|
||||||
|
Assert.Equal(kdfIterations, userDecryptionOptions.MasterPasswordUnlock.Kdf.Iterations);
|
||||||
|
Assert.Equal(kdfMemory, userDecryptionOptions.MasterPasswordUnlock.Kdf.Memory);
|
||||||
|
Assert.Equal(kdfParallelism, userDecryptionOptions.MasterPasswordUnlock.Kdf.Parallelism);
|
||||||
|
Assert.Equal(_mockEncryptedString, userDecryptionOptions.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
|
||||||
|
Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt);
|
||||||
|
}
|
||||||
|
|
||||||
private BaseRequestValidationContextFake CreateContext(
|
private BaseRequestValidationContextFake CreateContext(
|
||||||
ValidatedTokenRequest tokenRequest,
|
ValidatedTokenRequest tokenRequest,
|
||||||
CustomValidatorRequestContext requestContext,
|
CustomValidatorRequestContext requestContext,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class UserDecryptionOptionsBuilderTests
|
|||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
|
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
|
||||||
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
|
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
|
||||||
|
var user = new User();
|
||||||
|
_builder.ForUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -285,4 +287,32 @@ public class UserDecryptionOptionsBuilderTests
|
|||||||
|
|
||||||
Assert.True(result.TrustedDeviceOption?.HasAdminApproval);
|
Assert.True(result.TrustedDeviceOption?.HasAdminApproval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Build_WhenUserHasNoMasterPassword_ShouldReturnNoMasterPasswordUnlock(User user)
|
||||||
|
{
|
||||||
|
user.MasterPassword = null;
|
||||||
|
|
||||||
|
var result = await _builder.ForUser(user).BuildAsync();
|
||||||
|
|
||||||
|
Assert.False(result.HasMasterPassword);
|
||||||
|
Assert.Null(result.MasterPasswordUnlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Build_WhenUserHasMasterPassword_ShouldReturnMasterPasswordUnlock(User user)
|
||||||
|
{
|
||||||
|
user.Email = "test@example.COM";
|
||||||
|
|
||||||
|
var result = await _builder.ForUser(user).BuildAsync();
|
||||||
|
|
||||||
|
Assert.True(result.HasMasterPassword);
|
||||||
|
Assert.NotNull(result.MasterPasswordUnlock);
|
||||||
|
Assert.Equal(user.Kdf, result.MasterPasswordUnlock.Kdf.KdfType);
|
||||||
|
Assert.Equal(user.KdfIterations, result.MasterPasswordUnlock.Kdf.Iterations);
|
||||||
|
Assert.Equal(user.KdfMemory, result.MasterPasswordUnlock.Kdf.Memory);
|
||||||
|
Assert.Equal(user.KdfParallelism, result.MasterPasswordUnlock.Kdf.Parallelism);
|
||||||
|
Assert.Equal("test@example.com", result.MasterPasswordUnlock.Salt);
|
||||||
|
Assert.Equal(user.Key, result.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user