Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
63 changes: 42 additions & 21 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -54,6 +56,8 @@ public AccountsController(
IUserService userService,
IPolicyService policyService,
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
ITdeSetPasswordCommand tdeSetPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
Expand All @@ -69,6 +73,8 @@ IUserRepository userRepository
_userService = userService;
_policyService = policyService;
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
_tdeSetPasswordCommand = tdeSetPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
Expand Down Expand Up @@ -208,41 +214,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.IsTdeSetPasswordRequest())
{
await _tdeSetPasswordCommand.SetMasterPasswordAsync(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,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
};
}
}
40 changes: 0 additions & 40 deletions src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
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
@@ -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);
}
Loading
Loading