Skip to content
Open
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
66 changes: 61 additions & 5 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.TempPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
Expand All @@ -25,6 +26,7 @@
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Auth.Controllers;
Expand All @@ -37,11 +39,13 @@ public class AccountsController : Controller
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IUserService _userService;
private readonly ISelfServicePasswordChangeCommand _selfServicePasswordChangeCommand;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;
private readonly IFinishSsoJitProvisionMasterPasswordCommand _finishSsoJitProvisionMasterPasswordCommand;
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IReplaceAdminSetTemporaryPasswordCommand _replaceAdminSetTemporaryPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
Expand All @@ -54,11 +58,13 @@ public AccountsController(
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IUserService userService,
ISelfServicePasswordChangeCommand selfServicePasswordChangeCommand,
IPolicyService policyService,
IFinishSsoJitProvisionMasterPasswordCommand finishSsoJitProvisionMasterPasswordCommand,
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
ITdeSetPasswordCommand tdeSetPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
IReplaceAdminSetTemporaryPasswordCommand replaceAdminSetTemporaryPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IUserAccountKeysQuery userAccountKeysQuery,
Expand All @@ -71,11 +77,13 @@ IUserRepository userRepository
_organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository;
_userService = userService;
_selfServicePasswordChangeCommand = selfServicePasswordChangeCommand;
_policyService = policyService;
_finishSsoJitProvisionMasterPasswordCommand = finishSsoJitProvisionMasterPasswordCommand;
_setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
_tdeSetPasswordCommand = tdeSetPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_replaceAdminSetTemporaryPasswordCommand = replaceAdminSetTemporaryPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userAccountKeysQuery = userAccountKeysQuery;
Expand Down Expand Up @@ -197,8 +205,22 @@ public async Task PostPassword([FromBody] PasswordRequestModel model)
throw new UnauthorizedAccessException();
}

var result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.MasterPasswordHint, model.Key);
IdentityResult result;
if (model.RequestHasNewDataTypes())
{
result = await _selfServicePasswordChangeCommand.ChangePasswordAsync(
user,
model.MasterPasswordHash,
model.UnlockData!.ToData(),
model.AuthenticationData!.ToData(),
model.MasterPasswordHint);
}
else
{
result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.MasterPasswordHint, model.Key);
}

if (result.Succeeded)
{
return;
Expand Down Expand Up @@ -288,7 +310,7 @@ public async Task<MasterPasswordPolicyResponseModel> PostVerifyPassword([FromBod
}

[HttpPost("kdf")]
public async Task PostKdf([FromBody] PasswordRequestModel model)
public async Task PostKdf([FromBody] ChangeKdfRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
Expand Down Expand Up @@ -646,7 +668,24 @@ public async Task PutUpdateTempPasswordAsync([FromBody] UpdateTempPasswordReques
throw new UnauthorizedAccessException();
}

var result = await _userService.UpdateTempPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);
IdentityResult result;
if (model.RequestHasNewDataTypes())
{
result = await _replaceAdminSetTemporaryPasswordCommand.ReplaceTemporaryPasswordAsync(
user,
model.UnlockData!.ToData(),
model.AuthenticationData!.ToData(),
model.MasterPasswordHint);
}
else
{
result = await _userService.UpdateTempPasswordAsync(
user,
model.NewMasterPasswordHash,
model.Key,
model.MasterPasswordHint);
}

if (result.Succeeded)
{
return;
Expand All @@ -669,7 +708,24 @@ public async Task PutUpdateTdePasswordAsync([FromBody] UpdateTdeOffboardingPassw
throw new UnauthorizedAccessException();
}

var result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);
IdentityResult result;
if (model.RequestHasNewDataTypes())
{
result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(
user,
model.UnlockData!.ToData(),
model.AuthenticationData!.ToData(),
model.MasterPasswordHint);
}
else
{
result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(
user,
model.NewMasterPasswordHash,
model.Key,
model.MasterPasswordHint);
}

if (result.Succeeded)
{
return;
Expand Down
22 changes: 22 additions & 0 deletions src/Api/Auth/Controllers/EmergencyAccessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,28 @@ public async Task<EmergencyAccessTakeoverResponseModel> Takeover(Guid id)
public async Task Password(Guid id, [FromBody] EmergencyAccessPasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);

if (model.RequestHasNewDataTypes())
{
var result = await _emergencyAccessService.FinishRecoveryTakeoverAsync(
id,
user,
model.UnlockData!.ToData(),
model.AuthenticationData!.ToData());

if (result.Succeeded)
{
return;
}

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

throw new BadRequestException(ModelState);
}

await _emergencyAccessService.PasswordAsync(id, user, model.NewMasterPasswordHash, model.Key);
}

Expand Down
50 changes: 50 additions & 0 deletions src/Api/Auth/Models/Request/Accounts/ChangeKdfRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Api.Auth.Models.Request.Accounts;

/// <summary>
/// Dual-shape request: validation accepts either the legacy
/// (<see cref="NewMasterPasswordHash"/>, <see cref="Key"/>) or new
/// (<see cref="AuthenticationData"/>, <see cref="UnlockData"/>) payload so the wire contract
/// can stabilize ahead of caller wiring. <c>PostKdf</c> currently honors only the new shape;
/// legacy-shape dispatch arrives with <c>ChangeKdfCommand</c>'s dual-path refactor. All legacy
/// fields are removed in PM-33141.
/// </summary>
public class ChangeKdfRequestModel : IValidatableObject
{
[Required]
public required string MasterPasswordHash { get; set; }
[Obsolete("To be removed in PM-33141")]
[StringLength(300)]
public string? NewMasterPasswordHash { get; set; }
[Obsolete("To be removed in PM-33141")]
public string? Key { get; set; }

// Should be made required in PM-33141
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
// Should be made required in PM-33141
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var hasNewPayloads = AuthenticationData is not null && UnlockData is not null;
var hasLegacyPayloads = NewMasterPasswordHash is not null && Key is not null;

foreach (var validationResult in MasterPasswordPayloadVariantValidator.ValidateExclusivity(
hasNewPayloads, hasLegacyPayloads))
{
yield return validationResult;
}

if (hasNewPayloads)
{
foreach (var validationResult in KdfSettingsValidator.ValidateAuthenticationAndUnlockData(
AuthenticationData!.ToData(), UnlockData!.ToData()))
{
yield return validationResult;
}
}
}
}
52 changes: 25 additions & 27 deletions src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Api.Auth.Models.Request.Accounts;

public class PasswordRequestModel : SecretVerificationRequestModel
public class PasswordRequestModel : IValidatableObject
{
[Required]
public required string MasterPasswordHash { get; set; }
[Obsolete("To be removed in PM-33141")]
[StringLength(300)]
public required string NewMasterPasswordHash { get; set; }
public string? NewMasterPasswordHash { get; set; }
[Obsolete("To be removed in PM-33141")]
public string? Key { get; set; }
[StringLength(50)]
public string? MasterPasswordHint { get; set; }
[Required]
public required string Key { get; set; }

// Note: These will eventually become required, but not all consumers are moved over yet.
// Should be made required in PM-33141
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
// Should be made required in PM-33141
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }

public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
public bool RequestHasNewDataTypes()
{
foreach (var result in base.Validate(validationContext))
{
yield return result;
}
return UnlockData is not null && AuthenticationData is not null;
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var hasNewPayloads = AuthenticationData is not null && UnlockData is not null;
var hasLegacyPayloads = NewMasterPasswordHash is not null && Key is not null;

// Either both must be present or none should be present
if ((AuthenticationData == null) != (UnlockData == null))
foreach (var validationResult in MasterPasswordPayloadVariantValidator.ValidateExclusivity(
hasNewPayloads, hasLegacyPayloads))
{
yield return new ValidationResult(
"AuthenticationData and UnlockData must be provided.",
[nameof(AuthenticationData), nameof(UnlockData)]);
yield return validationResult;
}
if (AuthenticationData != null && UnlockData != null)
{
if (!AuthenticationData.HasSameKdfConfiguration(UnlockData))
{
yield return new ValidationResult(
"AuthenticationData and UnlockData must have the same KDF configuration.",
[nameof(AuthenticationData), nameof(UnlockData)]);
}

if (!AuthenticationData.Salt.Equals(UnlockData.Salt))
if (hasNewPayloads)
{
foreach (var validationResult in KdfSettingsValidator.ValidateAuthenticationAndUnlockData(
AuthenticationData!.ToData(), UnlockData!.ToData()))
{
yield return new ValidationResult(
"AuthenticationData and UnlockData must have the same salt.",
[nameof(AuthenticationData), nameof(UnlockData)]);
yield return validationResult;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Api.Auth.Models.Request.Accounts;

public class UpdateTdeOffboardingPasswordRequestModel
public class UpdateTdeOffboardingPasswordRequestModel : IValidatableObject
{
[Required]
[Obsolete("To be removed in PM-33141")]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }
[Required]
public string Key { get; set; }
public string? NewMasterPasswordHash { get; set; }
[Obsolete("To be removed in PM-33141")]
public string? Key { get; set; }
[StringLength(50)]
public string MasterPasswordHint { get; set; }
public string? MasterPasswordHint { get; set; }

// Should be made required in PM-33141
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
// Should be made required in PM-33141
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }

public bool RequestHasNewDataTypes()
{
return UnlockData is not null && AuthenticationData is not null;
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var hasNewPayloads = AuthenticationData is not null && UnlockData is not null;
var hasLegacyPayloads = NewMasterPasswordHash is not null && Key is not null;

foreach (var validationResult in MasterPasswordPayloadVariantValidator.ValidateExclusivity(
hasNewPayloads, hasLegacyPayloads))
{
yield return validationResult;
}

if (hasNewPayloads)
{
foreach (var validationResult in KdfSettingsValidator.ValidateAuthenticationAndUnlockData(
AuthenticationData!.ToData(), UnlockData!.ToData()))
{
yield return validationResult;
}
}
}
}
Loading
Loading