Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5ad515b
feat(master-password): Master Password Service - Initial implementation.
Patrick-Pimentel-Bitwarden Mar 26, 2026
41bdd72
misc changes, don't put me up
Patrick-Pimentel-Bitwarden Mar 26, 2026
39c61c7
feat(master-password): Master Password Service - Added data payloads.…
Patrick-Pimentel-Bitwarden Mar 30, 2026
2b83f8c
Merge remote-tracking branch 'origin' into auth/poc/master-password-s…
Patrick-Pimentel-Bitwarden Mar 30, 2026
d5245b6
feat(master-password): Master Password Service - Changed a comment or…
Patrick-Pimentel-Bitwarden Mar 30, 2026
2c2ef07
feat(master-password): Master Password Service - Master password serv…
Patrick-Pimentel-Bitwarden Mar 30, 2026
53d4806
feat(master-password): Master Password Service - Added in more fixes …
Patrick-Pimentel-Bitwarden Apr 2, 2026
380035d
Merge branch 'main' into auth/poc/master-password-service-example
Patrick-Pimentel-Bitwarden Apr 3, 2026
f31c6c4
feat(master-password): Master Password Service - Added in more change…
Patrick-Pimentel-Bitwarden Apr 7, 2026
0cd0954
Merge remote-tracking branch 'origin' into auth/poc/master-password-s…
Patrick-Pimentel-Bitwarden Apr 8, 2026
909ea5a
feat(master-password): Master Password Service - Lots of misc changes…
Patrick-Pimentel-Bitwarden Apr 9, 2026
3f6a1e0
feat(master-password): Master Password Service - Removed commands and…
Patrick-Pimentel-Bitwarden Apr 9, 2026
8385cae
feat(master-password): Master Password Service - Fixed validation error
Patrick-Pimentel-Bitwarden Apr 9, 2026
cb98256
feat(master-password): Master Password Service - Made changed to the …
Patrick-Pimentel-Bitwarden Apr 9, 2026
8fb2b71
feat(master-password): Master Password Service - Fixes to the self se…
Patrick-Pimentel-Bitwarden Apr 9, 2026
c6331ba
feat(master-password): Master Password Service - Added update temp pa…
Patrick-Pimentel-Bitwarden Apr 9, 2026
9252b93
Merge remote-tracking branch 'origin' into auth/poc/master-password-s…
Patrick-Pimentel-Bitwarden Apr 9, 2026
6f80b6d
test(master-password): Master Password Service - Updated the change k…
Patrick-Pimentel-Bitwarden Apr 10, 2026
d527c60
fix(master-password): Master Password Service - Updated update-temp-p…
Patrick-Pimentel-Bitwarden Apr 10, 2026
6feb4ce
fix(master-password): Master Password Service - Updated accounts cont…
Patrick-Pimentel-Bitwarden Apr 10, 2026
4d942f3
fix(master-password): Master Password Service - Fixed master password…
Patrick-Pimentel-Bitwarden Apr 10, 2026
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
20 changes: 16 additions & 4 deletions src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using AccountRecoveryV2 = Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2;
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
Expand Down Expand Up @@ -533,14 +534,25 @@ public async Task<IResult> PutRecoverAccount(Guid orgId, Guid id, [FromBody] Org
return Handle(await _adminRecoverAccountCommandV2.RecoverAccountAsync(commandRequest));
}

var result = await _adminRecoverAccountCommand.RecoverAccountAsync(
orgId, targetOrganizationUser, model.NewMasterPasswordHash!, model.Key!);
if (result.Succeeded)
IdentityResult identityResult;
if (model.RequestHasNewDataTypes())
{
identityResult = await _adminRecoverAccountCommand.RecoverAccountAsync(
orgId, targetOrganizationUser, model.UnlockData!.ToData(), model.AuthenticationData!.ToData());
}
// To be removed in PM-33141
else
{
identityResult = await _adminRecoverAccountCommand.RecoverAccountAsync(
orgId, targetOrganizationUser, model.NewMasterPasswordHash!, model.Key!);
}

if (identityResult.Succeeded)
{
return TypedResults.Ok();
}

foreach (var error in result.Errors)
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
Expand Down
164 changes: 105 additions & 59 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
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.Data;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
Expand All @@ -25,64 +27,47 @@
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;

[Route("accounts")]
[Authorize(Policies.Application)]
public class AccountsController : Controller
public class AccountsController(
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IUserService userService,
IMasterPasswordService masterPasswordService,
IPolicyService policyService,
IFinishSsoJitProvisionMasterPasswordCommand finishSsoJitProvisionMasterPasswordCommand,
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
ITdeSetPasswordCommand tdeSetPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
IUpdateTempPasswordCommand updateTempPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery,
ITwoFactorEmailService twoFactorEmailService,
IChangeKdfCommand changeKdfCommand,
IUserRepository userRepository) : Controller
{
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IUserService _userService;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;
private readonly IFinishSsoJitProvisionMasterPasswordCommand _finishSsoJitProvisionMasterPasswordCommand;
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
private readonly IUserRepository _userRepository;

public AccountsController(
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IUserService userService,
IPolicyService policyService,
IFinishSsoJitProvisionMasterPasswordCommand finishSsoJitProvisionMasterPasswordCommand,
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
ITdeSetPasswordCommand tdeSetPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IUserAccountKeysQuery userAccountKeysQuery,
ITwoFactorEmailService twoFactorEmailService,
IChangeKdfCommand changeKdfCommand,
IUserRepository userRepository
)
{
_organizationService = organizationService;
_organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository;
_userService = userService;
_policyService = policyService;
_finishSsoJitProvisionMasterPasswordCommand = finishSsoJitProvisionMasterPasswordCommand;
_setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
_tdeSetPasswordCommand = tdeSetPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userAccountKeysQuery = userAccountKeysQuery;
_twoFactorEmailService = twoFactorEmailService;
_changeKdfCommand = changeKdfCommand;
_userRepository = userRepository;
}
private readonly IOrganizationService _organizationService = organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository = providerUserRepository;
private readonly IUserService _userService = userService;
private readonly IMasterPasswordService _masterPasswordService = masterPasswordService;
private readonly IPolicyService _policyService = policyService;
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;
private readonly IFinishSsoJitProvisionMasterPasswordCommand _finishSsoJitProvisionMasterPasswordCommand = finishSsoJitProvisionMasterPasswordCommand;
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand = tdeSetPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
private readonly IUpdateTempPasswordCommand _updateTempPasswordCommand = updateTempPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
private readonly IUserAccountKeysQuery _userAccountKeysQuery = userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand = changeKdfCommand;
private readonly IUserRepository _userRepository = userRepository;


[HttpPost("password-hint")]
Expand Down Expand Up @@ -188,6 +173,16 @@ public async Task PostVerifyEmailToken([FromBody] VerifyEmailRequestModel model)
throw new BadRequestException(ModelState);
}

/// <summary>
/// For a user updating an existing password.
///
/// If calling this when a user does not have a master password, it will fail.
/// I don't think is a new boundary that has been introduced.
/// Need to double check this / get feedback from Jared.
/// </summary>
/// <param name="model"></param>
/// <exception cref="UnauthorizedAccessException"></exception>
/// <exception cref="BadRequestException"></exception>
[HttpPost("password")]
public async Task PostPassword([FromBody] PasswordRequestModel model)
{
Expand All @@ -197,8 +192,31 @@ 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())
{
// Jared, I'm unsure if check password should be turned into a query as a part of this work.
if (await _userService.CheckPasswordAsync(user, model.AuthenticationData!.MasterPasswordAuthenticationHash))
{
result = await _masterPasswordService.UpdateExistingMasterPasswordAndSaveAsync(user, new UpdateExistingPasswordData
{
MasterPasswordUnlock = model.UnlockData!.ToData(),
MasterPasswordAuthentication = model.AuthenticationData!.ToData(),
MasterPasswordHint = model.MasterPasswordHint
});
}
else
{
throw new BadRequestException("Passwords do not match.");
}
}
// To be removed in PM-33141
else
{
result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.MasterPasswordHint, model.Key);
}

if (result.Succeeded)
{
return;
Expand All @@ -222,7 +240,7 @@ public async Task PostSetPasswordAsync([FromBody] SetInitialPasswordRequestModel
throw new UnauthorizedAccessException();
}

if (model.IsV2Request())
if (model.RequestHasNewDataTypes())
{
if (model.IsTdeSetPasswordRequest())
{
Expand All @@ -248,8 +266,8 @@ public async Task PostSetPasswordAsync([FromBody] SetInitialPasswordRequestModel

var result = await _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
model.MasterPasswordHash,
model.Key,
model.MasterPasswordHash!,
model.Key!,
model.OrgIdentifier);

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

[HttpPost("kdf")]
public async Task PostKdf([FromBody] PasswordRequestModel model)
public async Task PostKdf([FromBody] ChangeKdfRequestModel model)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to double check if this will break anything with the sdk

{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
Expand Down Expand Up @@ -646,7 +664,25 @@ 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 _updateTempPasswordCommand.UpdateTempPasswordAsync(
user,
model.UnlockData!.ToData(),
model.AuthenticationData!.ToData(),
model.MasterPasswordHint);
}
// To be removed in PM-33141
else
{
result = await _userService.UpdateTempPasswordAsync(
user,
model.NewMasterPasswordHash,
model.Key,
model.MasterPasswordHint);
}

if (result.Succeeded)
{
return;
Expand All @@ -669,7 +705,17 @@ 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);
}
// To be removed in PM-33141
else
{
result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(user, model.NewMasterPasswordHash!, model.Key!, model.MasterPasswordHint);
}

if (result.Succeeded)
{
return;
Expand Down
11 changes: 10 additions & 1 deletion src/Api/Auth/Controllers/EmergencyAccessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,16 @@ public async Task<EmergencyAccessTakeoverResponseModel> Takeover(Guid id)
public async Task Password(Guid id, [FromBody] EmergencyAccessPasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
await _emergencyAccessService.PasswordAsync(id, user, model.NewMasterPasswordHash, model.Key);

if (model.RequestHasNewDataTypes())
{
await _emergencyAccessService.FinishRecoveryTakeoverAsync(id, user, model.UnlockData!.ToData(), model.AuthenticationData!.ToData());
}
// To be removed in PM-33141
else
{
await _emergencyAccessService.FinishRecoveryTakeoverAsync(id, user, model.NewMasterPasswordHash!, model.Key!);
}
}

[HttpPost("{id}/view")]
Expand Down
39 changes: 39 additions & 0 deletions src/Api/Auth/Models/Request/Accounts/ChangeKdfRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
ο»Ώusing System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;

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

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

public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }

// To be removed in PM-33141
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;

if (hasNewPayloads && hasLegacyPayloads)
{
yield return new ValidationResult(
"Cannot provide both new payloads (UnlockData/AuthenticationData) and legacy payloads (NewMasterPasswordHash/Key).",
[nameof(AuthenticationData), nameof(UnlockData), nameof(NewMasterPasswordHash), nameof(Key)]);
}

if (!hasNewPayloads && !hasLegacyPayloads)
{
yield return new ValidationResult(
"Must provide either new payloads (UnlockData/AuthenticationData) or legacy payloads (NewMasterPasswordHash/Key).",
[nameof(AuthenticationData), nameof(UnlockData), nameof(NewMasterPasswordHash), nameof(Key)]);
}
}
}
38 changes: 33 additions & 5 deletions src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,45 @@

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.
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }

// To be removed in PM-33141
public bool RequestHasNewDataTypes()
{
return UnlockData is not null && AuthenticationData is not null;
}

// To be removed in PM-33141
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;

if (hasNewPayloads && hasLegacyPayloads)
{
yield return new ValidationResult(
"Cannot provide both new payloads (UnlockData/AuthenticationData) and legacy payloads (NewMasterPasswordHash/Key).",
[nameof(AuthenticationData), nameof(UnlockData), nameof(NewMasterPasswordHash), nameof(Key)]);
}

if (!hasNewPayloads && !hasLegacyPayloads)
{
yield return new ValidationResult(
"Must provide either new payloads (UnlockData/AuthenticationData) or legacy payloads (NewMasterPasswordHash/Key).",
[nameof(AuthenticationData), nameof(UnlockData), nameof(NewMasterPasswordHash), nameof(Key)]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public User ToUser(User existingUser)

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (IsV2Request())
if (RequestHasNewDataTypes())
{
// V2 registration

Expand Down Expand Up @@ -134,7 +134,7 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
}
}

public bool IsV2Request()
public bool RequestHasNewDataTypes()
{
// AccountKeys can be null for TDE users, so we don't check that here
return MasterPasswordAuthentication != null && MasterPasswordUnlock != null;
Expand Down
Loading
Loading