mirror of
https://github.com/bitwarden/server.git
synced 2026-01-11 19:57:01 +00:00
[PM-27281] Support v2 account encryption on JIT master password signups (#6777)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* 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:
parent
62ae828143
commit
2e92a53f11
25 changed files with 2642 additions and 279 deletions
|
|
@ -38,7 +38,9 @@ public class AccountsController : Controller
|
|||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
|
@ -54,6 +56,8 @@ public class AccountsController : Controller
|
|||
IUserService userService,
|
||||
IPolicyService policyService,
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
|
||||
ITdeSetPasswordCommand tdeSetPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
|
|
@ -69,6 +73,8 @@ public class AccountsController : Controller
|
|||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
|
||||
_tdeSetPasswordCommand = tdeSetPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
|
|
@ -208,7 +214,7 @@ public class AccountsController : Controller
|
|||
}
|
||||
|
||||
[HttpPost("set-password")]
|
||||
public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model)
|
||||
public async Task PostSetPasswordAsync([FromBody] SetInitialPasswordRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
|
|
@ -216,33 +222,48 @@ public class AccountsController : Controller
|
|||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
try
|
||||
if (model.IsV2Request())
|
||||
{
|
||||
user = model.ToUser(user);
|
||||
if (model.IsTdeSetPasswordRequest())
|
||||
{
|
||||
await _tdeSetPasswordCommand.SetMasterPasswordAsync(user, model.ToData());
|
||||
}
|
||||
else
|
||||
{
|
||||
await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, model.ToData());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327
|
||||
try
|
||||
{
|
||||
user = model.ToUser(user);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
var result = await _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
|
||||
user,
|
||||
model.MasterPasswordHash,
|
||||
model.Key,
|
||||
model.OrgIdentifier);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
user,
|
||||
model.MasterPasswordHash,
|
||||
model.Key,
|
||||
model.OrgIdentifier);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("verify-password")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class SetInitialPasswordRequestModel : IValidatableObject
|
||||
{
|
||||
// TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27327
|
||||
[Obsolete("Use MasterPasswordAuthentication instead")]
|
||||
[StringLength(300)]
|
||||
public string? MasterPasswordHash { get; set; }
|
||||
|
||||
[Obsolete("Use MasterPasswordUnlock instead")]
|
||||
public string? Key { get; set; }
|
||||
|
||||
[Obsolete("Use AccountKeys instead")]
|
||||
public KeysRequestModel? Keys { get; set; }
|
||||
|
||||
[Obsolete("Use MasterPasswordAuthentication instead")]
|
||||
public KdfType? Kdf { get; set; }
|
||||
|
||||
[Obsolete("Use MasterPasswordAuthentication instead")]
|
||||
public int? KdfIterations { get; set; }
|
||||
|
||||
[Obsolete("Use MasterPasswordAuthentication instead")]
|
||||
public int? KdfMemory { get; set; }
|
||||
|
||||
[Obsolete("Use MasterPasswordAuthentication instead")]
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; }
|
||||
public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; }
|
||||
public AccountKeysRequestModel? AccountKeys { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string OrgIdentifier { get; set; }
|
||||
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327
|
||||
public User ToUser(User existingUser)
|
||||
{
|
||||
existingUser.MasterPasswordHint = MasterPasswordHint;
|
||||
existingUser.Kdf = Kdf!.Value;
|
||||
existingUser.KdfIterations = KdfIterations!.Value;
|
||||
existingUser.KdfMemory = KdfMemory;
|
||||
existingUser.KdfParallelism = KdfParallelism;
|
||||
existingUser.Key = Key;
|
||||
Keys?.ToUser(existingUser);
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (IsV2Request())
|
||||
{
|
||||
// V2 registration
|
||||
|
||||
// Validate Kdf
|
||||
var authenticationKdf = MasterPasswordAuthentication!.Kdf.ToData();
|
||||
var unlockKdf = MasterPasswordUnlock!.Kdf.ToData();
|
||||
|
||||
// Currently, KDF settings are not saved separately for authentication and unlock and must therefore be equal
|
||||
if (!authenticationKdf.Equals(unlockKdf))
|
||||
{
|
||||
yield return new ValidationResult("KDF settings must be equal for authentication and unlock.",
|
||||
[$"{nameof(MasterPasswordAuthentication)}.{nameof(MasterPasswordAuthenticationDataRequestModel.Kdf)}",
|
||||
$"{nameof(MasterPasswordUnlock)}.{nameof(MasterPasswordUnlockDataRequestModel.Kdf)}"]);
|
||||
}
|
||||
|
||||
var authenticationValidationErrors = KdfSettingsValidator.Validate(authenticationKdf).ToList();
|
||||
if (authenticationValidationErrors.Count != 0)
|
||||
{
|
||||
yield return authenticationValidationErrors.First();
|
||||
}
|
||||
|
||||
var unlockValidationErrors = KdfSettingsValidator.Validate(unlockKdf).ToList();
|
||||
if (unlockValidationErrors.Count != 0)
|
||||
{
|
||||
yield return unlockValidationErrors.First();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
// V1 registration
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327
|
||||
if (string.IsNullOrEmpty(MasterPasswordHash))
|
||||
{
|
||||
yield return new ValidationResult("MasterPasswordHash must be supplied.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Key))
|
||||
{
|
||||
yield return new ValidationResult("Key must be supplied.");
|
||||
}
|
||||
|
||||
if (Kdf == null)
|
||||
{
|
||||
yield return new ValidationResult("Kdf must be supplied.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (KdfIterations == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfIterations must be supplied.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (Kdf == KdfType.Argon2id)
|
||||
{
|
||||
if (KdfMemory == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
if (KdfParallelism == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfParallelism must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
}
|
||||
|
||||
var validationErrors = KdfSettingsValidator
|
||||
.Validate(Kdf!.Value, KdfIterations!.Value, KdfMemory, KdfParallelism).ToList();
|
||||
if (validationErrors.Count != 0)
|
||||
{
|
||||
yield return validationErrors.First();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsV2Request()
|
||||
{
|
||||
// AccountKeys can be null for TDE users, so we don't check that here
|
||||
return MasterPasswordAuthentication != null && MasterPasswordUnlock != null;
|
||||
}
|
||||
|
||||
public bool IsTdeSetPasswordRequest()
|
||||
{
|
||||
return AccountKeys == null;
|
||||
}
|
||||
|
||||
public SetInitialMasterPasswordDataModel ToData()
|
||||
{
|
||||
return new SetInitialMasterPasswordDataModel
|
||||
{
|
||||
MasterPasswordAuthentication = MasterPasswordAuthentication!.ToData(),
|
||||
MasterPasswordUnlock = MasterPasswordUnlock!.ToData(),
|
||||
OrgSsoIdentifier = OrgIdentifier,
|
||||
AccountKeys = AccountKeys?.ToAccountKeysData(),
|
||||
MasterPasswordHint = MasterPasswordHint
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class SetPasswordRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
[StringLength(50)]
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public KeysRequestModel Keys { get; set; }
|
||||
[Required]
|
||||
public KdfType Kdf { get; set; }
|
||||
[Required]
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
public string OrgIdentifier { get; set; }
|
||||
|
||||
public User ToUser(User existingUser)
|
||||
{
|
||||
existingUser.MasterPasswordHint = MasterPasswordHint;
|
||||
existingUser.Kdf = Kdf;
|
||||
existingUser.KdfIterations = KdfIterations;
|
||||
existingUser.KdfMemory = KdfMemory;
|
||||
existingUser.KdfParallelism = KdfParallelism;
|
||||
existingUser.Key = Key;
|
||||
Keys?.ToUser(existingUser);
|
||||
return existingUser;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,11 @@ namespace Bit.Api.KeyManagement.Models.Requests;
|
|||
public class MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
public required KdfRequestModel Kdf { get; init; }
|
||||
[Required]
|
||||
public required string MasterPasswordAuthenticationHash { get; init; }
|
||||
[StringLength(256)] public required string Salt { get; init; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public required string Salt { get; init; }
|
||||
|
||||
public MasterPasswordAuthenticationData ToData()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ namespace Bit.Api.KeyManagement.Models.Requests;
|
|||
public class MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
public required KdfRequestModel Kdf { get; init; }
|
||||
[EncryptedString] public required string MasterKeyWrappedUserKey { get; init; }
|
||||
[StringLength(256)] public required string Salt { get; init; }
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public required string MasterKeyWrappedUserKey { get; init; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public required string Salt { get; init; }
|
||||
|
||||
public MasterPasswordUnlockData ToData()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for setting an initial master password for a user.
|
||||
/// </summary>
|
||||
public class SetInitialMasterPasswordDataModel
|
||||
{
|
||||
public required MasterPasswordAuthenticationData MasterPasswordAuthentication { get; set; }
|
||||
public required MasterPasswordUnlockData MasterPasswordUnlock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Organization SSO identifier.
|
||||
/// </summary>
|
||||
public required string OrgSsoIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User account keys. Required for Master Password decryption user.
|
||||
/// </summary>
|
||||
public required UserAccountKeysData? AccountKeys { get; set; }
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
}
|
||||
|
|
@ -1,19 +1,25 @@
|
|||
using Bit.Core.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Manages the setting of the initial master password for a <see cref="User"/> in an organization.</para>
|
||||
/// <para>This class is primarily invoked in two scenarios:</para>
|
||||
/// <para>1) In organizations configured with Single Sign-On (SSO) and master password decryption:
|
||||
/// <para>In organizations configured with Single Sign-On (SSO) and master password decryption:
|
||||
/// just in time (JIT) provisioned users logging in via SSO are required to set a master password.</para>
|
||||
/// <para>2) In organizations configured with SSO and trusted devices decryption:
|
||||
/// Users who are upgraded to have admin account recovery permissions must set a master password
|
||||
/// to ensure their ability to reset other users' accounts.</para>
|
||||
/// </summary>
|
||||
public interface ISetInitialMasterPasswordCommand
|
||||
{
|
||||
public Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,
|
||||
string orgSsoIdentifier);
|
||||
/// <summary>
|
||||
/// Sets the initial master password and account keys for the specified user.
|
||||
/// </summary>
|
||||
/// <param name="user">User to set the master password for</param>
|
||||
/// <param name="masterPasswordDataModel">Initial master password setup data</param>
|
||||
/// <returns>A task that completes when the operation succeeds</returns>
|
||||
/// <exception cref="BadRequestException">
|
||||
/// Thrown if the user's master password is already set, the organization is not found,
|
||||
/// the user is not a member of the organization, or the account keys are missing.
|
||||
/// </exception>
|
||||
public Task SetInitialMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
using Bit.Core.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Manages the setting of the initial master password for a <see cref="User"/> in an organization.</para>
|
||||
/// <para>This class is primarily invoked in two scenarios:</para>
|
||||
/// <para>1) In organizations configured with Single Sign-On (SSO) and master password decryption:
|
||||
/// just in time (JIT) provisioned users logging in via SSO are required to set a master password.</para>
|
||||
/// <para>2) In organizations configured with SSO and trusted devices decryption:
|
||||
/// Users who are upgraded to have admin account recovery permissions must set a master password
|
||||
/// to ensure their ability to reset other users' accounts.</para>
|
||||
/// </summary>
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327
|
||||
[Obsolete("Use ISetInitialMasterPasswordCommand instead")]
|
||||
public interface ISetInitialMasterPasswordCommandV1
|
||||
{
|
||||
public Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,
|
||||
string orgSsoIdentifier);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Manages the setting of the master password for a TDE <see cref="User"/> in an organization.</para>
|
||||
/// <para>In organizations configured with SSO and trusted devices decryption:
|
||||
/// Users who are upgraded to have admin account recovery permissions must set a master password
|
||||
/// to ensure their ability to reset other users' accounts.</para>
|
||||
/// </summary>
|
||||
public interface ITdeSetPasswordCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the master password for the specified TDE user.
|
||||
/// </summary>
|
||||
/// <param name="user">User to set the master password for</param>
|
||||
/// <param name="masterPasswordDataModel">Master password setup data</param>
|
||||
/// <returns>A task that completes when the operation succeeds</returns>
|
||||
/// <exception cref="BadRequestException">
|
||||
/// Thrown if the user's master password is already set, the organization is not found,
|
||||
/// the user is not a member of the organization, or the user is a TDE user without account keys set.
|
||||
/// </exception>
|
||||
Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
|
@ -6,98 +7,74 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
|
||||
public class SetInitialMasterPasswordCommand : ISetInitialMasterPasswordCommand
|
||||
{
|
||||
private readonly ILogger<SetInitialMasterPasswordCommand> _logger;
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
|
||||
public SetInitialMasterPasswordCommand(
|
||||
ILogger<SetInitialMasterPasswordCommand> logger,
|
||||
IdentityErrorDescriber identityErrorDescriber,
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
IEventService eventService,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
public SetInitialMasterPasswordCommand(IUserService userService, IUserRepository userRepository,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand, IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository, IPasswordHasher<User> passwordHasher,
|
||||
IEventService eventService)
|
||||
{
|
||||
_logger = logger;
|
||||
_identityErrorDescriber = identityErrorDescriber;
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_eventService = eventService;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,
|
||||
string orgSsoIdentifier)
|
||||
public async Task SetInitialMasterPasswordAsync(User user,
|
||||
SetInitialMasterPasswordDataModel masterPasswordDataModel)
|
||||
{
|
||||
if (user == null)
|
||||
if (user.Key != null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
throw new BadRequestException("User already has a master password set.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.MasterPassword))
|
||||
if (masterPasswordDataModel.AccountKeys == null)
|
||||
{
|
||||
_logger.LogWarning("Change password failed for user {userId} - already has password.", user.Id);
|
||||
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
|
||||
throw new BadRequestException("Account keys are required.");
|
||||
}
|
||||
|
||||
var result = await _userService.UpdatePasswordHash(user, masterPassword, validatePassword: true, refreshStamp: false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
|
||||
user.Key = key;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(orgSsoIdentifier))
|
||||
{
|
||||
throw new BadRequestException("Organization SSO Identifier required.");
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);
|
||||
// Prevent a de-synced salt value from creating an un-decryptable unlock method
|
||||
masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);
|
||||
masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);
|
||||
|
||||
var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization invalid.");
|
||||
throw new BadRequestException("Organization SSO identifier is invalid.");
|
||||
}
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);
|
||||
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User not found within organization.");
|
||||
}
|
||||
|
||||
// TDE users who go from a user without admin acct recovery permission to having it will be
|
||||
// required to set a MP for the first time and we don't want to re-execute the accept logic
|
||||
// as they are already confirmed.
|
||||
// TLDR: only accept post SSO user if they are invited
|
||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
}
|
||||
// Hash the provided user master password authentication hash on the server side
|
||||
var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user,
|
||||
masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);
|
||||
|
||||
return IdentityResult.Success;
|
||||
var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id,
|
||||
masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash,
|
||||
masterPasswordDataModel.MasterPasswordHint);
|
||||
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, masterPasswordDataModel.AccountKeys,
|
||||
[setMasterPasswordTask]);
|
||||
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||
|
||||
await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
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 Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
|
||||
public class SetInitialMasterPasswordCommandV1 : ISetInitialMasterPasswordCommandV1
|
||||
{
|
||||
private readonly ILogger<SetInitialMasterPasswordCommandV1> _logger;
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
|
||||
|
||||
public SetInitialMasterPasswordCommandV1(
|
||||
ILogger<SetInitialMasterPasswordCommandV1> logger,
|
||||
IdentityErrorDescriber identityErrorDescriber,
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
IEventService eventService,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_identityErrorDescriber = identityErrorDescriber;
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_eventService = eventService;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,
|
||||
string orgSsoIdentifier)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.MasterPassword))
|
||||
{
|
||||
_logger.LogWarning("Change password failed for user {userId} - already has password.", user.Id);
|
||||
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
|
||||
}
|
||||
|
||||
var result = await _userService.UpdatePasswordHash(user, masterPassword, validatePassword: true, refreshStamp: false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
|
||||
user.Key = key;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(orgSsoIdentifier))
|
||||
{
|
||||
throw new BadRequestException("Organization SSO Identifier required.");
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);
|
||||
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization invalid.");
|
||||
}
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);
|
||||
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User not found within organization.");
|
||||
}
|
||||
|
||||
// TDE users who go from a user without admin acct recovery permission to having it will be
|
||||
// required to set a MP for the first time and we don't want to re-execute the accept logic
|
||||
// as they are already confirmed.
|
||||
// TLDR: only accept post SSO user if they are invited
|
||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
|
||||
public class TdeSetPasswordCommand : ITdeSetPasswordCommand
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public TdeSetPasswordCommand(IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository,
|
||||
IPasswordHasher<User> passwordHasher, IEventService eventService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public async Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel)
|
||||
{
|
||||
if (user.Key != null)
|
||||
{
|
||||
throw new BadRequestException("User already has a master password set.");
|
||||
}
|
||||
|
||||
if (user.PublicKey == null || user.PrivateKey == null)
|
||||
{
|
||||
throw new BadRequestException("TDE user account keys must be set before setting initial master password.");
|
||||
}
|
||||
|
||||
// Prevent a de-synced salt value from creating an un-decryptable unlock method
|
||||
masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);
|
||||
masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);
|
||||
|
||||
var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization SSO identifier is invalid.");
|
||||
}
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User not found within organization.");
|
||||
}
|
||||
|
||||
// Hash the provided user master password authentication hash on the server side
|
||||
var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user,
|
||||
masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);
|
||||
|
||||
var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id,
|
||||
masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash,
|
||||
masterPasswordDataModel.MasterPasswordHint);
|
||||
await _userRepository.UpdateUserDataAsync([setMasterPasswordTask]);
|
||||
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,8 @@ public static class UserServiceCollectionExtensions
|
|||
private static void AddUserPasswordCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();
|
||||
services.AddScoped<ISetInitialMasterPasswordCommandV1, SetInitialMasterPasswordCommandV1>();
|
||||
services.AddScoped<ITdeSetPasswordCommand, TdeSetPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddTdeOffboardingPasswordCommands(this IServiceCollection services)
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ public static class FeatureFlagKeys
|
|||
public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit";
|
||||
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
|
||||
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
|
||||
public const string EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
|
|
|
|||
|
|
@ -74,6 +74,24 @@ public interface IUserRepository : IRepository<User, Guid>
|
|||
Task DeleteManyAsync(IEnumerable<User> users);
|
||||
|
||||
UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the master password and KDF for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <param name="masterPasswordUnlockData">Data for unlocking with the master password.</param>
|
||||
/// <param name="serverSideHashedMasterPasswordAuthenticationHash">Server side hash of the user master authentication password hash</param>
|
||||
/// <param name="masterPasswordHint">Optional hint for the master password.</param>
|
||||
/// <returns>A task to complete the operation.</returns>
|
||||
UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,
|
||||
string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint);
|
||||
|
||||
/// <summary>
|
||||
/// Updates multiple user data properties in a single transaction.
|
||||
/// </summary>
|
||||
/// <param name="updateUserDataActions">Actions to update user data.</param>
|
||||
/// <returns>On success</returns>
|
||||
Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions);
|
||||
}
|
||||
|
||||
public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null,
|
||||
|
|
|
|||
|
|
@ -428,6 +428,55 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
|||
};
|
||||
}
|
||||
|
||||
public UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,
|
||||
string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint)
|
||||
{
|
||||
return async (connection, transaction) =>
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
await connection!.ExecuteAsync(
|
||||
"[dbo].[User_UpdateMasterPassword]",
|
||||
new
|
||||
{
|
||||
Id = userId,
|
||||
MasterPassword = serverSideHashedMasterPasswordAuthenticationHash,
|
||||
MasterPasswordHint = masterPasswordHint,
|
||||
Key = masterPasswordUnlockData.MasterKeyWrappedUserKey,
|
||||
Kdf = masterPasswordUnlockData.Kdf.KdfType,
|
||||
KdfIterations = masterPasswordUnlockData.Kdf.Iterations,
|
||||
KdfMemory = masterPasswordUnlockData.Kdf.Memory,
|
||||
KdfParallelism = masterPasswordUnlockData.Kdf.Parallelism,
|
||||
RevisionDate = timestamp,
|
||||
AccountRevisionDate = timestamp
|
||||
},
|
||||
transaction: transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
foreach (var action in updateUserDataActions)
|
||||
{
|
||||
await action(connection, transaction);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
|
||||
{
|
||||
if (user == null)
|
||||
|
|
|
|||
|
|
@ -510,6 +510,51 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
|||
};
|
||||
}
|
||||
|
||||
public UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,
|
||||
string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint)
|
||||
{
|
||||
return async (_, _) =>
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var userEntity = await dbContext.Users.FindAsync(userId);
|
||||
if (userEntity == null)
|
||||
{
|
||||
throw new ArgumentException("User not found", nameof(userId));
|
||||
}
|
||||
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
userEntity.MasterPassword = serverSideHashedMasterPasswordAuthenticationHash;
|
||||
userEntity.MasterPasswordHint = masterPasswordHint;
|
||||
userEntity.Key = masterPasswordUnlockData.MasterKeyWrappedUserKey;
|
||||
userEntity.Kdf = masterPasswordUnlockData.Kdf.KdfType;
|
||||
userEntity.KdfIterations = masterPasswordUnlockData.Kdf.Iterations;
|
||||
userEntity.KdfMemory = masterPasswordUnlockData.Kdf.Memory;
|
||||
userEntity.KdfParallelism = masterPasswordUnlockData.Kdf.Parallelism;
|
||||
userEntity.RevisionDate = timestamp;
|
||||
userEntity.AccountRevisionDate = timestamp;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
foreach (var action in updateUserDataActions)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable<Guid> userIds)
|
||||
{
|
||||
var defaultCollections = (from c in dbContext.Collections
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
CREATE PROCEDURE [dbo].[User_UpdateMasterPassword]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@MasterPassword NVARCHAR(300),
|
||||
@MasterPasswordHint NVARCHAR(50) = NULL,
|
||||
@Key VARCHAR(MAX),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT = NULL,
|
||||
@KdfParallelism INT = NULL,
|
||||
@RevisionDate DATETIME2(7),
|
||||
@AccountRevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[User]
|
||||
SET
|
||||
[MasterPassword] = @MasterPassword,
|
||||
[MasterPasswordHint] = @MasterPasswordHint,
|
||||
[Key] = @Key,
|
||||
[Kdf] = @Kdf,
|
||||
[KdfIterations] = @KdfIterations,
|
||||
[KdfMemory] = @KdfMemory,
|
||||
[KdfParallelism] = @KdfParallelism,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[AccountRevisionDate] = @AccountRevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
CREATE OR ALTER PROCEDURE [dbo].[User_UpdateMasterPassword]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@MasterPassword NVARCHAR(300),
|
||||
@MasterPasswordHint NVARCHAR(50) = NULL,
|
||||
@Key VARCHAR(MAX),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT = NULL,
|
||||
@KdfParallelism INT = NULL,
|
||||
@RevisionDate DATETIME2(7),
|
||||
@AccountRevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[User]
|
||||
SET
|
||||
[MasterPassword] = @MasterPassword,
|
||||
[MasterPasswordHint] = @MasterPasswordHint,
|
||||
[Key] = @Key,
|
||||
[Kdf] = @Kdf,
|
||||
[KdfIterations] = @KdfIterations,
|
||||
[KdfMemory] = @KdfMemory,
|
||||
[KdfParallelism] = @KdfParallelism,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[AccountRevisionDate] = @AccountRevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
Loading…
Add table
Reference in a new issue