1
0
mirror of https://github.com/bitwarden/server synced 2026-01-18 08:23:21 +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,19 +1,28 @@
using System.Net;
using System.Text.Json;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
using static Bit.Core.KeyManagement.Enums.SignatureAlgorithm;
namespace Bit.Api.IntegrationTest.Controllers;
@@ -21,6 +30,8 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
{
private static readonly string _masterKeyWrappedUserKey =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
private static readonly string _mockEncryptedType7WrappedSigningKey = "7.DRv74Kg1RSlFSam1MNFlGD==";
private static readonly string _masterPasswordHash = "master_password_hash";
private static readonly string _newMasterPasswordHash = "new_master_password_hash";
@@ -35,6 +46,11 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
private readonly IPushNotificationService _pushNotificationService;
private readonly IFeatureService _featureService;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
private readonly IEventRepository _eventRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private string _ownerEmail = null!;
@@ -49,6 +65,11 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
_pushNotificationService = _factory.GetService<IPushNotificationService>();
_featureService = _factory.GetService<IFeatureService>();
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
_organizationRepository = _factory.GetService<IOrganizationRepository>();
_ssoConfigRepository = _factory.GetService<ISsoConfigRepository>();
_userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();
_eventRepository = _factory.GetService<IEventRepository>();
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
}
public async Task InitializeAsync()
@@ -435,4 +456,531 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
message.Content = JsonContent.Create(requestModel);
return await _client.SendAsync(message);
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_MasterPasswordDecryption_Success(string organizationSsoIdentifier)
{
// Arrange - Create organization and user
var ownerEmail = $"owner-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
ownerEmail: ownerEmail,
name: "Test Org V1");
organization.UseSso = true;
organization.Identifier = organizationSsoIdentifier;
await _organizationRepository.ReplaceAsync(organization);
await _ssoConfigRepository.CreateAsync(new SsoConfig
{
OrganizationId = organization.Id,
Enabled = true,
Data = JsonSerializer.Serialize(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.MasterPassword,
}, JsonHelpers.CamelCase),
});
// Create user with password initially, so we can login
var userEmail = $"user-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(userEmail);
// Add user to organization
var user = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(user);
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);
// Login as the user
await _loginHelper.LoginAsync(userEmail);
// Remove the master password and keys to simulate newly registered SSO user
user.MasterPassword = null;
user.Key = null;
user.PrivateKey = null;
user.PublicKey = null;
await _userRepository.ReplaceAsync(user);
// V1 (Obsolete) request format - to be removed with PM-27327
var request = new
{
masterPasswordHash = _newMasterPasswordHash,
key = _masterKeyWrappedUserKey,
keys = new
{
publicKey = "v1-publicKey",
encryptedPrivateKey = "v1-encryptedPrivateKey"
},
kdf = 0, // PBKDF2_SHA256
kdfIterations = 600000,
kdfMemory = (int?)null,
kdfParallelism = (int?)null,
masterPasswordHint = "v1-integration-test-hint",
orgIdentifier = organization.Identifier
};
var jsonRequest = JsonSerializer.Serialize(request, JsonHelpers.CamelCase);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
Assert.Fail($"Expected success but got {response.StatusCode}. Error: {errorContent}");
}
// Verify user in database
var updatedUser = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(updatedUser);
Assert.Equal("v1-integration-test-hint", updatedUser.MasterPasswordHint);
// Verify the master password is hashed and stored
Assert.NotNull(updatedUser.MasterPassword);
var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);
Assert.Equal(PasswordVerificationResult.Success, verificationResult);
// Verify KDF settings
Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);
Assert.Equal(600_000, updatedUser.KdfIterations);
Assert.Null(updatedUser.KdfMemory);
Assert.Null(updatedUser.KdfParallelism);
// Verify timestamps are updated
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
// Verify keys are set (V1 uses Keys property)
Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);
Assert.Equal("v1-publicKey", updatedUser.PublicKey);
Assert.Equal("v1-encryptedPrivateKey", updatedUser.PrivateKey);
// Verify User_ChangedPassword event was logged
var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });
Assert.NotNull(events);
Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);
// Verify user was accepted into the organization
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);
var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);
Assert.NotNull(orgUser);
Assert.Equal(OrganizationUserStatusType.Accepted, orgUser.Status);
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_MasterPasswordDecryption_Success(string organizationSsoIdentifier)
{
// Arrange - Create organization and user
var ownerEmail = $"owner-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
ownerEmail: ownerEmail,
name: "Test Org");
organization.UseSso = true;
organization.Identifier = organizationSsoIdentifier;
await _organizationRepository.ReplaceAsync(organization);
await _ssoConfigRepository.CreateAsync(new SsoConfig
{
OrganizationId = organization.Id,
Enabled = true,
Data = JsonSerializer.Serialize(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.MasterPassword,
}, JsonHelpers.CamelCase),
});
// Create user with password initially, so we can login
var userEmail = $"user-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(userEmail);
// Add user to organization
var user = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(user);
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);
// Login as the user
await _loginHelper.LoginAsync(userEmail);
// Remove the master password and keys to simulate newly registered SSO user
user.MasterPassword = null;
user.Key = null;
user.PrivateKey = null;
user.PublicKey = null;
user.SignedPublicKey = null;
await _userRepository.ReplaceAsync(user);
var jsonRequest = CreateV2SetPasswordRequestJson(
userEmail,
organization.Identifier,
"integration-test-hint",
includeAccountKeys: true);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
Assert.Fail($"Expected success but got {response.StatusCode}. Error: {errorContent}");
}
// Verify user in database
var updatedUser = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(updatedUser);
Assert.Equal("integration-test-hint", updatedUser.MasterPasswordHint);
// Verify the master password is hashed and stored
Assert.NotNull(updatedUser.MasterPassword);
var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);
Assert.Equal(PasswordVerificationResult.Success, verificationResult);
// Verify KDF settings
Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);
Assert.Equal(600_000, updatedUser.KdfIterations);
Assert.Null(updatedUser.KdfMemory);
Assert.Null(updatedUser.KdfParallelism);
// Verify timestamps are updated
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
// Verify keys are set
Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);
Assert.Equal("publicKey", updatedUser.PublicKey);
Assert.Equal(_mockEncryptedType7String, updatedUser.PrivateKey);
Assert.Equal("signedPublicKey", updatedUser.SignedPublicKey);
// Verify security state
Assert.Equal(2, updatedUser.SecurityVersion);
Assert.Equal("v2", updatedUser.SecurityState);
// Verify signature key pair data
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(updatedUser.Id);
Assert.NotNull(signatureKeyPair);
Assert.Equal(Ed25519, signatureKeyPair.SignatureAlgorithm);
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
Assert.Equal("verifyingKey", signatureKeyPair.VerifyingKey);
// Verify User_ChangedPassword event was logged
var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });
Assert.NotNull(events);
Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);
// Verify user was accepted into the organization
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);
var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);
Assert.NotNull(orgUser);
Assert.Equal(OrganizationUserStatusType.Accepted, orgUser.Status);
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_TDEDecryption_Success(string organizationSsoIdentifier)
{
// Arrange - Create organization with TDE
var ownerEmail = $"owner-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
ownerEmail: ownerEmail,
name: "Test Org TDE");
organization.UseSso = true;
organization.Identifier = organizationSsoIdentifier;
await _organizationRepository.ReplaceAsync(organization);
// Configure SSO for TDE (TrustedDeviceEncryption)
await _ssoConfigRepository.CreateAsync(new SsoConfig
{
OrganizationId = organization.Id,
Enabled = true,
Data = JsonSerializer.Serialize(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}, JsonHelpers.CamelCase),
});
// Create user with password initially, so we can login
var userEmail = $"user-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(userEmail);
var user = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(user);
// Add user to organization and confirm them (TDE users are confirmed, not invited)
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Confirmed);
// Login as the user
await _loginHelper.LoginAsync(userEmail);
// Set up TDE user with V2 account keys but no master password
// TDE users already have their account keys from device provisioning
user.MasterPassword = null;
user.Key = null;
user.PublicKey = "tde-publicKey";
user.PrivateKey = _mockEncryptedType7String;
user.SignedPublicKey = "tde-signedPublicKey";
user.SecurityVersion = 2;
user.SecurityState = "v2-tde";
await _userRepository.ReplaceAsync(user);
// Create signature key pair for TDE user
var signatureKeyPairData = new Core.KeyManagement.Models.Data.SignatureKeyPairData(
Ed25519,
_mockEncryptedType7WrappedSigningKey,
"tde-verifyingKey");
var setSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
if (setSignatureKeyPair == null)
{
var newKeyPair = new Core.KeyManagement.Entities.UserSignatureKeyPair
{
UserId = user.Id,
SignatureAlgorithm = signatureKeyPairData.SignatureAlgorithm,
SigningKey = signatureKeyPairData.WrappedSigningKey,
VerifyingKey = signatureKeyPairData.VerifyingKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};
newKeyPair.SetNewId();
await _userSignatureKeyPairRepository.CreateAsync(newKeyPair);
}
var jsonRequest = CreateV2SetPasswordRequestJson(
userEmail,
organization.Identifier,
"tde-test-hint",
includeAccountKeys: false);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
Assert.Fail($"Expected success but got {response.StatusCode}. Error: {errorContent}");
}
// Verify user in database
var updatedUser = await _userRepository.GetByEmailAsync(userEmail);
Assert.NotNull(updatedUser);
Assert.Equal("tde-test-hint", updatedUser.MasterPasswordHint);
// Verify the master password is hashed and stored
Assert.NotNull(updatedUser.MasterPassword);
var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);
Assert.Equal(PasswordVerificationResult.Success, verificationResult);
// Verify KDF settings
Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);
Assert.Equal(600_000, updatedUser.KdfIterations);
Assert.Null(updatedUser.KdfMemory);
Assert.Null(updatedUser.KdfParallelism);
// Verify timestamps are updated
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
// Verify key is set
Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);
// Verify AccountKeys are preserved (TDE users already had V2 keys)
Assert.Equal("tde-publicKey", updatedUser.PublicKey);
Assert.Equal(_mockEncryptedType7String, updatedUser.PrivateKey);
Assert.Equal("tde-signedPublicKey", updatedUser.SignedPublicKey);
Assert.Equal(2, updatedUser.SecurityVersion);
Assert.Equal("v2-tde", updatedUser.SecurityState);
// Verify signature key pair is preserved (TDE users already had signature keys)
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(updatedUser.Id);
Assert.NotNull(signatureKeyPair);
Assert.Equal(Ed25519, signatureKeyPair.SignatureAlgorithm);
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
Assert.Equal("tde-verifyingKey", signatureKeyPair.VerifyingKey);
// Verify User_ChangedPassword event was logged
var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });
Assert.NotNull(events);
Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);
// Verify user remains confirmed in the organization
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);
var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);
Assert.NotNull(orgUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status);
}
[Fact]
public async Task PostSetPasswordAsync_V2_Unauthorized_ReturnsUnauthorized()
{
// Arrange - Don't login
var jsonRequest = CreateV2SetPasswordRequestJson(
"test@bitwarden.com",
"test-org-identifier",
"test-hint",
includeAccountKeys: true);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task PostSetPasswordAsync_V2_MismatchedKdfSettings_ReturnsBadRequest()
{
// Arrange
var email = $"kdf-mismatch-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
await _loginHelper.LoginAsync(email);
// Test mismatched KDF settings (600000 vs 650000 iterations)
var request = new
{
masterPasswordAuthentication = new
{
kdf = new
{
kdfType = 0,
iterations = 600000
},
masterPasswordAuthenticationHash = _newMasterPasswordHash,
salt = email
},
masterPasswordUnlock = new
{
kdf = new
{
kdfType = 0,
iterations = 650000 // Different from authentication KDF
},
masterKeyWrappedUserKey = _masterKeyWrappedUserKey,
salt = email
},
accountKeys = new
{
userKeyEncryptedAccountPrivateKey = "7.AOs41Hd8OQiCPXjyJKCiDA==",
accountPublicKey = "public-key"
},
orgIdentifier = "test-org-identifier"
};
var jsonRequest = JsonSerializer.Serialize(request, JsonHelpers.CamelCase);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 1, null, null)]
[InlineData(KdfType.Argon2id, 4, null, 5)]
[InlineData(KdfType.Argon2id, 4, 65, null)]
public async Task PostSetPasswordAsync_V2_InvalidKdfSettings_ReturnsBadRequest(
KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
// Arrange
var email = $"invalid-kdf-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
await _loginHelper.LoginAsync(email);
var jsonRequest = CreateV2SetPasswordRequestJson(
email,
"test-org-identifier",
"test-hint",
includeAccountKeys: true,
kdfType: kdf,
kdfIterations: kdfIterations,
kdfMemory: kdfMemory,
kdfParallelism: kdfParallelism);
// Act
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/set-password");
message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
var response = await _client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static string CreateV2SetPasswordRequestJson(
string userEmail,
string orgIdentifier,
string hint,
bool includeAccountKeys = true,
KdfType? kdfType = null,
int? kdfIterations = null,
int? kdfMemory = null,
int? kdfParallelism = null)
{
var kdf = new
{
kdfType = (int)(kdfType ?? KdfType.PBKDF2_SHA256),
iterations = kdfIterations ?? 600000,
memory = kdfMemory,
parallelism = kdfParallelism
};
var request = new
{
masterPasswordAuthentication = new
{
kdf,
masterPasswordAuthenticationHash = _newMasterPasswordHash,
salt = userEmail
},
masterPasswordUnlock = new
{
kdf,
masterKeyWrappedUserKey = _masterKeyWrappedUserKey,
salt = userEmail
},
accountKeys = includeAccountKeys ? new
{
accountPublicKey = "publicKey",
userKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,
publicKeyEncryptionKeyPair = new
{
publicKey = "publicKey",
wrappedPrivateKey = _mockEncryptedType7String,
signedPublicKey = "signedPublicKey"
},
signatureKeyPair = new
{
signatureAlgorithm = "ed25519",
wrappedSigningKey = _mockEncryptedType7WrappedSigningKey,
verifyingKey = "verifyingKey"
},
securityState = new
{
securityVersion = 2,
securityState = "v2"
}
} : null,
masterPasswordHint = hint,
orgIdentifier
};
return JsonSerializer.Serialize(request, JsonHelpers.CamelCase);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,682 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.Auth.Models.Request.Accounts;
public class SetInitialPasswordRequestModelTests
{
#region V2 Validation Tests
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void Validate_V2Request_WithMatchingKdf_ReturnsNoErrors(KdfType kdfType, int iterations, int? memory, int? parallelism)
{
// Arrange
var kdf = new KdfRequestModel
{
KdfType = kdfType,
Iterations = iterations,
Memory = memory,
Parallelism = parallelism
};
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = "orgIdentifier",
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
},
AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "privateKey",
AccountPublicKey = "publicKey"
}
};
// Act
var result = model.Validate(new ValidationContext(model));
// Assert
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public void Validate_V2Request_WithMismatchedKdfSettings_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 650000 // Different iterations
},
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
}
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Single(result);
Assert.Contains("KDF settings must be equal", result[0].ErrorMessage);
var memberNames = result[0].MemberNames.ToList();
Assert.Equal(2, memberNames.Count);
Assert.Contains("MasterPasswordAuthentication.Kdf", memberNames);
Assert.Contains("MasterPasswordUnlock.Kdf", memberNames);
}
[Theory]
[BitAutoData]
public void Validate_V2Request_WithInvalidAuthenticationKdf_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 1 // Too low
};
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
}
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.NotEmpty(result);
Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KDF iterations must be between"));
}
#endregion
#region V1 Validation Tests (Obsolete)
[Theory]
[BitAutoData]
public void Validate_V1Request_WithMissingMasterPasswordHash_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
Key = "key",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 600000
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage.Contains("MasterPasswordHash must be supplied"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithMissingKey_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 600000
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage.Contains("Key must be supplied"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithMissingKdf_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
KdfIterations = 600000
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("Kdf must be supplied"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithMissingKdfIterations_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
Kdf = KdfType.PBKDF2_SHA256
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KdfIterations must be supplied"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithArgon2idAndMissingMemory_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
Kdf = KdfType.Argon2id,
KdfIterations = 3,
KdfParallelism = 4
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage.Contains("KdfMemory must be supplied when Kdf is Argon2id"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithArgon2idAndMissingParallelism_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
Kdf = KdfType.Argon2id,
KdfIterations = 3,
KdfMemory = 64
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.Contains(result, r => r.ErrorMessage.Contains("KdfParallelism must be supplied when Kdf is Argon2id"));
}
[Theory]
[BitAutoData]
public void Validate_V1Request_WithInvalidKdfSettings_ReturnsValidationError(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 5000 // Too low
};
// Act
var result = model.Validate(new ValidationContext(model)).ToList();
// Assert
Assert.NotEmpty(result);
Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KDF iterations must be between"));
}
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void Validate_V1Request_WithValidSettings_ReturnsNoErrors(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = "orgIdentifier",
MasterPasswordHash = "hash",
Key = "key",
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism
};
// Act
var result = model.Validate(new ValidationContext(model));
// Assert
Assert.Empty(result);
}
#endregion
#region IsV2Request Tests
[Theory]
[BitAutoData]
public void IsV2Request_WithV2Properties_ReturnsTrue(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
}
};
// Act
var result = model.IsV2Request();
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData]
public void IsV2Request_WithoutMasterPasswordAuthentication_ReturnsFalse(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
}
};
// Act
var result = model.IsV2Request();
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public void IsV2Request_WithoutMasterPasswordUnlock_ReturnsFalse(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
}
};
// Act
var result = model.IsV2Request();
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public void IsV2Request_WithV1Properties_ReturnsFalse(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHash = "hash",
Key = "key",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 600000
};
// Act
var result = model.IsV2Request();
// Assert
Assert.False(result);
}
#endregion
#region IsTdeSetPasswordRequest Tests
[Theory]
[BitAutoData]
public void IsTdeSetPasswordRequest_WithNullAccountKeys_ReturnsTrue(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
},
AccountKeys = null
};
// Act
var result = model.IsTdeSetPasswordRequest();
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData]
public void IsTdeSetPasswordRequest_WithAccountKeys_ReturnsFalse(string orgIdentifier)
{
// Arrange
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
},
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
},
AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "privateKey",
AccountPublicKey = "publicKey"
}
};
// Act
var result = model.IsTdeSetPasswordRequest();
// Assert
Assert.False(result);
}
#endregion
#region ToUser Tests (Obsolete)
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void ToUser_WithKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
// Arrange
var existingUser = new User();
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = "orgIdentifier",
MasterPasswordHash = "hash",
MasterPasswordHint = "hint",
Key = "key",
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Keys = new KeysRequestModel
{
PublicKey = "publicKey",
EncryptedPrivateKey = "encryptedPrivateKey"
}
};
// Act
var result = model.ToUser(existingUser);
// Assert
Assert.Same(existingUser, result);
Assert.Equal("hint", result.MasterPasswordHint);
Assert.Equal(kdfType, result.Kdf);
Assert.Equal(kdfIterations, result.KdfIterations);
Assert.Equal(kdfMemory, result.KdfMemory);
Assert.Equal(kdfParallelism, result.KdfParallelism);
Assert.Equal("key", result.Key);
Assert.Equal("publicKey", result.PublicKey);
Assert.Equal("encryptedPrivateKey", result.PrivateKey);
}
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void ToUser_WithoutKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
// Arrange
var existingUser = new User();
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = "orgIdentifier",
MasterPasswordHash = "hash",
MasterPasswordHint = "hint",
Key = "key",
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Keys = null
};
// Act
var result = model.ToUser(existingUser);
// Assert
Assert.Same(existingUser, result);
Assert.Equal("hint", result.MasterPasswordHint);
Assert.Equal(kdfType, result.Kdf);
Assert.Equal(kdfIterations, result.KdfIterations);
Assert.Equal(kdfMemory, result.KdfMemory);
Assert.Equal(kdfParallelism, result.KdfParallelism);
Assert.Equal("key", result.Key);
Assert.Null(result.PublicKey);
Assert.Null(result.PrivateKey);
}
#endregion
#region ToData Tests
[Theory]
[BitAutoData]
public void ToData_MapsPropertiesCorrectly(string orgIdentifier)
{
// Arrange
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
};
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHint = "hint",
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
},
AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "privateKey",
AccountPublicKey = "publicKey"
}
};
// Act
var result = model.ToData();
// Assert
Assert.NotNull(result);
Assert.Equal(orgIdentifier, result.OrgSsoIdentifier);
Assert.Equal("hint", result.MasterPasswordHint);
Assert.NotNull(result.MasterPasswordAuthentication);
Assert.NotNull(result.MasterPasswordUnlock);
Assert.NotNull(result.AccountKeys);
Assert.Equal("authHash", result.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);
Assert.Equal("wrappedKey", result.MasterPasswordUnlock.MasterKeyWrappedUserKey);
}
[Theory]
[BitAutoData]
public void ToData_WithNullAccountKeys_MapsCorrectly(string orgIdentifier)
{
// Arrange
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
};
var model = new SetInitialPasswordRequestModel
{
OrgIdentifier = orgIdentifier,
MasterPasswordHint = "hint",
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
},
AccountKeys = null
};
// Act
var result = model.ToData();
// Assert
Assert.NotNull(result);
Assert.Equal(orgIdentifier, result.OrgSsoIdentifier);
Assert.Null(result.AccountKeys);
}
#endregion
}

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
};
}
}