Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TdeOnboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
Expand All @@ -38,7 +39,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 ITdeOnboardingPasswordCommand _tdeOnboardingPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
Expand All @@ -54,6 +57,8 @@ public AccountsController(
IUserService userService,
IPolicyService policyService,
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
ITdeOnboardingPasswordCommand tdeOnboardingPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
Expand All @@ -69,6 +74,8 @@ IUserRepository userRepository
_userService = userService;
_policyService = policyService;
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
_tdeOnboardingPasswordCommand = tdeOnboardingPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
Expand Down Expand Up @@ -208,41 +215,56 @@ public async Task PostPassword([FromBody] PasswordRequestModel model)
}

[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)
{
throw new UnauthorizedAccessException();
}

try
if (model.IsV2Request())
{
user = model.ToUser(user);
if (model.IsTdeOnboardingPassword())
{
await _tdeOnboardingPasswordCommand.OnboardMasterPasswordAsync(user, model.ToData());
}
else
{
await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, model.ToData());
}
}
catch (Exception e)
else
{
ModelState.AddModelError(string.Empty, e.Message);
throw new BadRequestException(ModelState);
}
// 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 _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
user,
model.MasterPasswordHash,
model.Key,
model.OrgIdentifier);
var result = await _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
model.MasterPasswordHash,
model.Key,
model.OrgIdentifier);

if (result.Succeeded)
{
return;
}
if (result.Succeeded)
{
return;
}

foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}

throw new BadRequestException(ModelState);
throw new BadRequestException(ModelState);
}
}

[HttpPost("verify-password")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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.Exceptions;
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))
{
throw new BadRequestException("KDF settings must be equal for authentication and unlock.");
}

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.");
}

if (KdfIterations == null)
{
yield return new ValidationResult("KdfIterations must be supplied.");
}

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 IsTdeOnboardingPassword()
{
return AccountKeys == null;
}

public SetInitialMasterPasswordDataModel ToData()
{
return new SetInitialMasterPasswordDataModel
{
MasterPasswordAuthentication = MasterPasswordAuthentication!.ToData(),
MasterPasswordUnlock = MasterPasswordUnlock!.ToData(),
OrgSsoIdentifier = OrgIdentifier,
AccountKeys = AccountKeys?.ToAccountKeysData(),
MasterPasswordHint = MasterPasswordHint
};
}
}
40 changes: 0 additions & 40 deletions src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs

This file was deleted.

23 changes: 23 additions & 0 deletions src/Core/Auth/Models/Data/SetInitialMasterPasswordDataModel.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Exceptions;

namespace Bit.Core.Auth.UserFeatures.TdeOnboardingPassword.Interfaces;

/// <summary>
/// <para>Manages the setting of the master password onboarding 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 ITdeOnboardingPasswordCommand
{
/// <summary>
/// Onboard 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, the master password does not meet requirements,
/// or the user is a TDE user without account keys set.
/// </exception>
Task OnboardMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel);
}
Loading
Loading