mirror of
https://github.com/bitwarden/server.git
synced 2026-04-12 20:53:32 -05:00
feat(master-password): Master Password Service - Added update temp password update
This commit is contained in:
@@ -41,6 +41,7 @@ using Bit.Core.Utilities;
|
||||
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;
|
||||
@@ -533,14 +534,25 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Bit.Core.Auth.Identity;
|
||||
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;
|
||||
@@ -44,6 +45,7 @@ public class AccountsController(
|
||||
ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,
|
||||
ITdeSetPasswordCommand tdeSetPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IUpdateTempPasswordCommand updateTempPasswordCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
ITwoFactorEmailService twoFactorEmailService,
|
||||
@@ -60,6 +62,7 @@ public class AccountsController(
|
||||
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;
|
||||
@@ -643,7 +646,25 @@ public class AccountsController(
|
||||
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;
|
||||
@@ -666,12 +687,12 @@ public class AccountsController(
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@@ -3,7 +3,7 @@ using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class PasswordRequestModel
|
||||
public class PasswordRequestModel : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public required string MasterPasswordHash { get; set; }
|
||||
|
||||
@@ -16,8 +16,8 @@ public class OrganizationUserResetPasswordRequestModel : IValidatableObject
|
||||
[Obsolete("To be removed in PM-33141")]
|
||||
public string? Key { get; set; }
|
||||
|
||||
public MasterPasswordUnlockDataRequestModel? UnlockData;
|
||||
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData;
|
||||
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }
|
||||
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
|
||||
|
||||
public RecoverAccountRequest ToCommandRequest(Guid orgId, OrganizationUser organizationUser) => new()
|
||||
{
|
||||
@@ -33,6 +33,12 @@ public class OrganizationUserResetPasswordRequestModel : IValidatableObject
|
||||
AuthenticationData = AuthenticationData,
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ public interface IAdminRecoverAccountCommand
|
||||
/// </summary>
|
||||
/// <param name="orgId">The organization the user belongs to.</param>
|
||||
/// <param name="organizationUser">The organization user being recovered.</param>
|
||||
/// <param name="newMasterPassword">The user's new master password hash.</param>
|
||||
/// <param name="key">The user's new master-password-sealed user key.</param>
|
||||
/// <param name="unlockData">The user's new master-password unlock data.</param>
|
||||
/// <param name="authenticationData">The user's new master-password authentication data.</param>
|
||||
/// <returns>An IdentityResult indicating success or failure.</returns>
|
||||
/// <exception cref="BadRequestException">When organization settings, policy, or user state is invalid.</exception>
|
||||
/// <exception cref="NotFoundException">When the user does not exist.</exception>
|
||||
|
||||
@@ -68,6 +68,7 @@ public class AdminRecoverAccountCommand(
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// To be removed in PM-33141
|
||||
else
|
||||
{
|
||||
var result = await HandlePayloadWithDeprecatedRawDataAsync(user, request);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2;
|
||||
|
||||
public record RecoverAccountRequest : IValidatableObject
|
||||
public record RecoverAccountRequest
|
||||
{
|
||||
public required Guid OrgId { get; init; }
|
||||
public required OrganizationUser OrganizationUser { get; init; }
|
||||
@@ -24,25 +23,4 @@ public record RecoverAccountRequest : IValidatableObject
|
||||
{
|
||||
return UnlockData is not null && AuthenticationData is not null;
|
||||
}
|
||||
|
||||
// To be removed in PM-33141
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
var hasNewPayloads = UnlockData is not null && AuthenticationData 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(UnlockData), nameof(AuthenticationData), nameof(NewMasterPasswordHash), nameof(Key)]);
|
||||
}
|
||||
|
||||
if (!hasNewPayloads && !hasLegacyPayloads)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Must provide either new payloads (UnlockData/AuthenticationData) or legacy payloads (NewMasterPasswordHash/Key).",
|
||||
[nameof(UnlockData), nameof(AuthenticationData), nameof(NewMasterPasswordHash), nameof(Key)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
/// </summary>
|
||||
public interface ITdeOffboardingPasswordCommand
|
||||
{
|
||||
[Obsolete("To be removed in PM-33141")]
|
||||
public Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string masterPassword, string key,
|
||||
string? masterPasswordHint);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ public class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand
|
||||
}
|
||||
|
||||
[Obsolete("To be removed in PM-33141")]
|
||||
public async Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string newMasterPassword, string key, string hint)
|
||||
public async Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string newMasterPassword, string key, string? hint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newMasterPassword))
|
||||
{
|
||||
@@ -108,7 +108,7 @@ public class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand
|
||||
User user,
|
||||
MasterPasswordUnlockData unlockData,
|
||||
MasterPasswordAuthenticationData authenticationData,
|
||||
string hint)
|
||||
string? masterPasswordHint)
|
||||
{
|
||||
var orgUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id);
|
||||
orgUserDetails = orgUserDetails.Where(x => x.UseSso).ToList();
|
||||
@@ -117,35 +117,35 @@ public class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand
|
||||
throw new BadRequestException("User is not part of any organization that has SSO enabled.");
|
||||
}
|
||||
|
||||
var orgSSOUsers = await Task.WhenAll(orgUserDetails.Select(async x => await _ssoUserRepository.GetByUserIdOrganizationIdAsync(x.OrganizationId, user.Id)));
|
||||
if (orgSSOUsers.Length != 1)
|
||||
var orgSsoUsers = await Task.WhenAll(orgUserDetails.Select(async x => await _ssoUserRepository.GetByUserIdOrganizationIdAsync(x.OrganizationId, user.Id)));
|
||||
if (orgSsoUsers.Length != 1)
|
||||
{
|
||||
throw new BadRequestException("User is part of no or multiple SSO configurations.");
|
||||
}
|
||||
|
||||
var orgUser = orgUserDetails.First();
|
||||
var orgSSOConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgUser.OrganizationId);
|
||||
if (orgSSOConfig == null)
|
||||
var orgSsoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgUser.OrganizationId);
|
||||
if (orgSsoConfig == null)
|
||||
{
|
||||
throw new BadRequestException("Organization SSO configuration not found.");
|
||||
}
|
||||
|
||||
if (orgSSOConfig.GetData().MemberDecryptionType != Enums.MemberDecryptionType.MasterPassword)
|
||||
if (orgSsoConfig.GetData().MemberDecryptionType != Enums.MemberDecryptionType.MasterPassword)
|
||||
{
|
||||
throw new BadRequestException("Organization SSO Member Decryption Type is not Master Password.");
|
||||
}
|
||||
|
||||
// We only want to be setting an initial master password here, if they already have one,
|
||||
// we are in an error state.
|
||||
var result = await _masterPasswordService.OnlyMutateUserSetInitialMasterPasswordAsync(user, new SetInitialPasswordData
|
||||
var identityResult = await _masterPasswordService.OnlyMutateUserSetInitialMasterPasswordAsync(user, new SetInitialPasswordData
|
||||
{
|
||||
MasterPasswordUnlock = unlockData,
|
||||
MasterPasswordAuthentication = authenticationData,
|
||||
MasterPasswordHint = hint
|
||||
MasterPasswordHint = masterPasswordHint
|
||||
});
|
||||
if (!result.Succeeded)
|
||||
if (!identityResult.Succeeded)
|
||||
{
|
||||
return result;
|
||||
return identityResult;
|
||||
}
|
||||
|
||||
// Side effect of running TDE offboarding, we want to force reset
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.TempPassword.Interfaces;
|
||||
|
||||
public interface IUpdateTempPasswordCommand
|
||||
{
|
||||
Task<IdentityResult> UpdateTempPasswordAsync(
|
||||
User user,
|
||||
MasterPasswordUnlockData unlockData,
|
||||
MasterPasswordAuthenticationData authenticationData,
|
||||
string? masterPasswordHint);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Bit.Core.Auth.UserFeatures.TempPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.TempPassword;
|
||||
|
||||
public class UpdateTempPasswordCommand(
|
||||
IMasterPasswordService masterPasswordService,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IEventService eventService,
|
||||
IPushNotificationService pushService) : IUpdateTempPasswordCommand
|
||||
{
|
||||
public async Task<IdentityResult> UpdateTempPasswordAsync(
|
||||
User user,
|
||||
MasterPasswordUnlockData unlockData,
|
||||
MasterPasswordAuthenticationData authenticationData,
|
||||
string? masterPasswordHint)
|
||||
{
|
||||
if (!user.ForcePasswordReset)
|
||||
{
|
||||
throw new BadRequestException("User does not have a temporary password to update.");
|
||||
}
|
||||
|
||||
var result = await masterPasswordService.OnlyMutateUserUpdateExistingMasterPasswordAsync(user, new UpdateExistingPasswordData
|
||||
{
|
||||
MasterPasswordUnlock = unlockData,
|
||||
MasterPasswordAuthentication = authenticationData,
|
||||
MasterPasswordHint = masterPasswordHint,
|
||||
});
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
user.ForcePasswordReset = false;
|
||||
|
||||
await userRepository.ReplaceAsync(user);
|
||||
await mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name ?? string.Empty);
|
||||
await eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);
|
||||
await pushService.PushLogOutAsync(user.Id);
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.Registration;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TempPassword;
|
||||
using Bit.Core.Auth.UserFeatures.TempPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -28,6 +30,7 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddDeviceTrustCommands();
|
||||
services.AddEmergencyAccessCommands();
|
||||
services.AddUserPasswordCommands();
|
||||
services.AddUpdateTempPasswordCommands();
|
||||
services.AddUserRegistrationCommands();
|
||||
services.AddWebAuthnLoginCommands();
|
||||
services.AddTdeOffboardingPasswordCommands();
|
||||
@@ -63,6 +66,11 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddScoped<ITdeOffboardingPasswordCommand, TdeOffboardingPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddUpdateTempPasswordCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUpdateTempPasswordCommand, UpdateTempPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddUserRegistrationCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISendVerificationEmailForRegistrationCommand, SendVerificationEmailForRegistrationCommand>();
|
||||
|
||||
@@ -35,6 +35,7 @@ public interface IUserService
|
||||
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
|
||||
Task<IdentityResult> ConvertToKeyConnectorAsync(User user, string keyConnectorKeyWrappedUserKey);
|
||||
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
|
||||
[Obsolete("To be removed in PM-33141")]
|
||||
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
|
||||
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
||||
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
|
||||
|
||||
@@ -658,6 +658,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
[Obsolete("To be removed in PM-33141")]
|
||||
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
|
||||
{
|
||||
if (!user.ForcePasswordReset)
|
||||
|
||||
Reference in New Issue
Block a user