1
0
mirror of https://github.com/bitwarden/server synced 2026-02-18 18:33:29 +00:00

Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response (#6093)

This commit is contained in:
Maciej Zieniuk
2025-07-28 17:34:42 +02:00
committed by GitHub
parent d407c164b6
commit 59e7bc7438
7 changed files with 247 additions and 27 deletions

View File

@@ -36,7 +36,15 @@ public class IdentityServerSsoTests
public async Task Test_MasterPassword_DecryptionType()
{
// 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
// 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:
// "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);
// One property for the Object and one for master password
Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count());
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
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(TestEmail, salt);
}
[Fact]

View File

@@ -67,7 +67,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
Assert.Equal(0, kdf);
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, kdfIterations);
AssertUserDecryptionOptions(root);
AssertUserDecryptionOptions(root, user);
}
[Theory, RegisterFinishRequestModelCustomize]
@@ -601,14 +601,27 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
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)
.EnumerateObject();
var userDecryptionOptions =
AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object);
Assert.Collection(userDecryptionOptions,
(prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); },
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
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)

View File

@@ -3,10 +3,13 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
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.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -27,6 +30,9 @@ namespace Bit.Identity.Test.IdentityServer;
public class BaseRequestValidatorTests
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private UserManager<User> _userManager;
private readonly IUserService _userService;
private readonly IEventService _eventService;
@@ -377,6 +383,102 @@ public class BaseRequestValidatorTests
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(
ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,

View File

@@ -28,6 +28,8 @@ public class UserDecryptionOptionsBuilderTests
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
var user = new User();
_builder.ForUser(user);
}
[Theory]
@@ -285,4 +287,32 @@ public class UserDecryptionOptionsBuilderTests
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);
}
}