mirror of
https://github.com/bitwarden/server
synced 2026-01-03 17:14:00 +00:00
[PM-23249] Prevent log-out when changing KDF settings (#6349)
* Prevent log-out when changing KDF settings with feature flag. * validate salt unchanged for user to throw bad request (400), not internal server error (500) * change kdf integration tests * failing tests * iuncorrect tests wording * conditional logout * log out reason as enum * explicit naming
This commit is contained in:
6
src/Core/Enums/PushNotificationLogOutReason.cs
Normal file
6
src/Core/Enums/PushNotificationLogOutReason.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum PushNotificationLogOutReason : byte
|
||||
{
|
||||
KdfChange = 0
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -18,17 +19,22 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
private readonly ILogger<ChangeKdfCommand> _logger;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, IUserRepository userRepository, IdentityErrorDescriber describer, ILogger<ChangeKdfCommand> logger)
|
||||
public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService,
|
||||
IUserRepository userRepository, IdentityErrorDescriber describer, ILogger<ChangeKdfCommand> logger,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userService = userService;
|
||||
_pushService = pushService;
|
||||
_userRepository = userRepository;
|
||||
_identityErrorDescriber = describer;
|
||||
_logger = logger;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData)
|
||||
public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash,
|
||||
MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash))
|
||||
@@ -37,8 +43,8 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
||||
}
|
||||
|
||||
// Validate to prevent user account from becoming un-decryptable from invalid parameters
|
||||
//
|
||||
// Prevent a de-synced salt value from creating an un-decryptable unlock method
|
||||
//
|
||||
// Prevent a de-synced salt value from creating an un-decryptable unlock method
|
||||
authenticationData.ValidateSaltUnchangedForUser(user);
|
||||
unlockData.ValidateSaltUnchangedForUser(user);
|
||||
|
||||
@@ -47,12 +53,15 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
||||
{
|
||||
throw new BadRequestException("KDF settings must be equal for authentication and unlock.");
|
||||
}
|
||||
|
||||
var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf);
|
||||
if (validationErrors.Any())
|
||||
{
|
||||
throw new BadRequestException("KDF settings are invalid.");
|
||||
}
|
||||
|
||||
var logoutOnKdfChange = !_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
|
||||
|
||||
// Update the user with the new KDF settings
|
||||
// This updates the authentication data and unlock data for the user separately. Currently these still
|
||||
// use shared values for KDF settings and salt.
|
||||
@@ -68,7 +77,8 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
||||
// This entire operation MUST be atomic to prevent a user from being locked out of their account.
|
||||
// Salt is ensured to be the same as unlock data, and the value stored in the account and not updated.
|
||||
// KDF is ensured to be the same as unlock data above and updated below.
|
||||
var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash);
|
||||
var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash,
|
||||
refreshStamp: logoutOnKdfChange);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
_logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
|
||||
@@ -88,7 +98,17 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
||||
user.LastKdfChangeDate = now;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
if (logoutOnKdfChange)
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clients that support the new feature flag will ignore the logout when it matches the reason and the feature flag is enabled.
|
||||
await _pushService.PushLogOutAsync(user.Id, reason: PushNotificationLogOutReason.KdfChange);
|
||||
await _pushService.PushSyncSettingsAsync(user.Id);
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
@@ -12,7 +13,7 @@ public class MasterPasswordAuthenticationData
|
||||
{
|
||||
if (user.GetMasterPasswordSalt() != Salt)
|
||||
{
|
||||
throw new ArgumentException("Invalid master password salt.");
|
||||
throw new BadRequestException("Invalid master password salt.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
@@ -14,7 +13,7 @@ public class MasterPasswordUnlockData
|
||||
{
|
||||
if (user.GetMasterPasswordSalt() != Salt)
|
||||
{
|
||||
throw new ArgumentException("Invalid master password salt.");
|
||||
throw new BadRequestException("Invalid master password salt.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,3 +97,9 @@ public class ProviderBankAccountVerifiedPushNotification
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid AdminId { get; set; }
|
||||
}
|
||||
|
||||
public class LogOutPushNotification
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public PushNotificationLogOutReason? Reason { get; set; }
|
||||
}
|
||||
|
||||
@@ -167,18 +167,17 @@ public interface IPushNotificationService
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false,
|
||||
PushNotificationLogOutReason? reason = null)
|
||||
=> PushAsync(new PushNotification<LogOutPushNotification>
|
||||
{
|
||||
Type = PushType.LogOut,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
Payload = new LogOutPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
#pragma warning disable BWP0001 // Type or member is obsolete
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
#pragma warning restore BWP0001 // Type or member is obsolete
|
||||
Reason = reason
|
||||
},
|
||||
ExcludeCurrentContext = excludeCurrentContextFromPush,
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ public enum PushType : byte
|
||||
[NotificationInfo("not-specified", typeof(Models.UserPushNotification))]
|
||||
SyncSettings = 10,
|
||||
|
||||
[NotificationInfo("not-specified", typeof(Models.UserPushNotification))]
|
||||
[NotificationInfo("not-specified", typeof(Models.LogOutPushNotification))]
|
||||
LogOut = 11,
|
||||
|
||||
[NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))]
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class HubHelpers
|
||||
case PushType.SyncSettings:
|
||||
case PushType.LogOut:
|
||||
var userNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(
|
||||
JsonSerializer.Deserialize<PushNotificationData<LogOutPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, userNotification, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user