mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 15:55:10 -06: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:
parent
8d52ae869c
commit
6324f692b8
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.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Models.Data;
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
@ -18,17 +19,22 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||||
private readonly ILogger<ChangeKdfCommand> _logger;
|
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;
|
_userService = userService;
|
||||||
_pushService = pushService;
|
_pushService = pushService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_identityErrorDescriber = describer;
|
_identityErrorDescriber = describer;
|
||||||
_logger = logger;
|
_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);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash))
|
if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash))
|
||||||
@ -47,12 +53,15 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("KDF settings must be equal for authentication and unlock.");
|
throw new BadRequestException("KDF settings must be equal for authentication and unlock.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf);
|
var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf);
|
||||||
if (validationErrors.Any())
|
if (validationErrors.Any())
|
||||||
{
|
{
|
||||||
throw new BadRequestException("KDF settings are invalid.");
|
throw new BadRequestException("KDF settings are invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logoutOnKdfChange = !_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
|
||||||
|
|
||||||
// Update the user with the new KDF settings
|
// Update the user with the new KDF settings
|
||||||
// This updates the authentication data and unlock data for the user separately. Currently these still
|
// This updates the authentication data and unlock data for the user separately. Currently these still
|
||||||
// use shared values for KDF settings and salt.
|
// 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.
|
// 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.
|
// 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.
|
// 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)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
|
_logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
|
||||||
@ -88,7 +98,17 @@ public class ChangeKdfCommand : IChangeKdfCommand
|
|||||||
user.LastKdfChangeDate = now;
|
user.LastKdfChangeDate = now;
|
||||||
|
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
|
if (logoutOnKdfChange)
|
||||||
|
{
|
||||||
await _pushService.PushLogOutAsync(user.Id);
|
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;
|
return IdentityResult.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.KeyManagement.Models.Data;
|
namespace Bit.Core.KeyManagement.Models.Data;
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ public class MasterPasswordAuthenticationData
|
|||||||
{
|
{
|
||||||
if (user.GetMasterPasswordSalt() != Salt)
|
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.Exceptions;
|
||||||
using Bit.Core.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Core.KeyManagement.Models.Data;
|
namespace Bit.Core.KeyManagement.Models.Data;
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ public class MasterPasswordUnlockData
|
|||||||
{
|
{
|
||||||
if (user.GetMasterPasswordSalt() != Salt)
|
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 ProviderId { get; set; }
|
||||||
public Guid AdminId { 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,
|
ExcludeCurrentContext = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false)
|
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false,
|
||||||
=> PushAsync(new PushNotification<UserPushNotification>
|
PushNotificationLogOutReason? reason = null)
|
||||||
|
=> PushAsync(new PushNotification<LogOutPushNotification>
|
||||||
{
|
{
|
||||||
Type = PushType.LogOut,
|
Type = PushType.LogOut,
|
||||||
Target = NotificationTarget.User,
|
Target = NotificationTarget.User,
|
||||||
TargetId = userId,
|
TargetId = userId,
|
||||||
Payload = new UserPushNotification
|
Payload = new LogOutPushNotification
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
#pragma warning disable BWP0001 // Type or member is obsolete
|
Reason = reason
|
||||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
|
||||||
#pragma warning restore BWP0001 // Type or member is obsolete
|
|
||||||
},
|
},
|
||||||
ExcludeCurrentContext = excludeCurrentContextFromPush,
|
ExcludeCurrentContext = excludeCurrentContextFromPush,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -55,7 +55,7 @@ public enum PushType : byte
|
|||||||
[NotificationInfo("not-specified", typeof(Models.UserPushNotification))]
|
[NotificationInfo("not-specified", typeof(Models.UserPushNotification))]
|
||||||
SyncSettings = 10,
|
SyncSettings = 10,
|
||||||
|
|
||||||
[NotificationInfo("not-specified", typeof(Models.UserPushNotification))]
|
[NotificationInfo("not-specified", typeof(Models.LogOutPushNotification))]
|
||||||
LogOut = 11,
|
LogOut = 11,
|
||||||
|
|
||||||
[NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))]
|
[NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))]
|
||||||
|
|||||||
@ -64,7 +64,7 @@ public static class HubHelpers
|
|||||||
case PushType.SyncSettings:
|
case PushType.SyncSettings:
|
||||||
case PushType.LogOut:
|
case PushType.LogOut:
|
||||||
var userNotification =
|
var userNotification =
|
||||||
JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(
|
JsonSerializer.Deserialize<PushNotificationData<LogOutPushNotification>>(
|
||||||
notificationJson, _deserializerOptions);
|
notificationJson, _deserializerOptions);
|
||||||
await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
|
await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
|
||||||
.SendAsync(_receiveMessageMethod, userNotification, cancellationToken);
|
.SendAsync(_receiveMessageMethod, userNotification, cancellationToken);
|
||||||
|
|||||||
@ -1,31 +1,81 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net;
|
||||||
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.IntegrationTest.Controllers;
|
namespace Bit.Api.IntegrationTest.Controllers;
|
||||||
|
|
||||||
public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||||
{
|
{
|
||||||
private readonly ApiApplicationFactory _factory;
|
private static readonly string _masterKeyWrappedUserKey =
|
||||||
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||||
|
|
||||||
public AccountsControllerTest(ApiApplicationFactory factory) => _factory = factory;
|
private static readonly string _masterPasswordHash = "master_password_hash";
|
||||||
|
private static readonly string _newMasterPasswordHash = "new_master_password_hash";
|
||||||
|
|
||||||
|
private static readonly KdfRequestModel _defaultKdfRequest =
|
||||||
|
new() { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 };
|
||||||
|
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
private readonly LoginHelper _loginHelper;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
|
|
||||||
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
|
public AccountsControllerTest(ApiApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_factory.SubstituteService<IPushNotificationService>(_ => { });
|
||||||
|
_factory.SubstituteService<IFeatureService>(_ => { });
|
||||||
|
_client = factory.CreateClient();
|
||||||
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
|
_userRepository = _factory.GetService<IUserRepository>();
|
||||||
|
_pushNotificationService = _factory.GetService<IPushNotificationService>();
|
||||||
|
_featureService = _factory.GetService<IFeatureService>();
|
||||||
|
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAccountsProfile_success()
|
public async Task GetAccountsProfile_success()
|
||||||
{
|
{
|
||||||
var tokens = await _factory.LoginWithNewAccount();
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
var client = _factory.CreateClient();
|
|
||||||
|
|
||||||
using var message = new HttpRequestMessage(HttpMethod.Get, "/accounts/profile");
|
using var message = new HttpRequestMessage(HttpMethod.Get, "/accounts/profile");
|
||||||
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
var response = await _client.SendAsync(message);
|
||||||
var response = await client.SendAsync(message);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
|
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
|
||||||
Assert.NotNull(content);
|
Assert.NotNull(content);
|
||||||
Assert.Equal("integration-test@bitwarden.com", content.Email);
|
Assert.Equal(_ownerEmail, content.Email);
|
||||||
Assert.NotNull(content.Name);
|
Assert.NotNull(content.Name);
|
||||||
Assert.True(content.EmailVerified);
|
Assert.True(content.EmailVerified);
|
||||||
Assert.False(content.Premium);
|
Assert.False(content.Premium);
|
||||||
@ -35,4 +85,354 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
|||||||
Assert.NotNull(content.PrivateKey);
|
Assert.NotNull(content.PrivateKey);
|
||||||
Assert.NotNull(content.SecurityStamp);
|
Assert.NotNull(content.SecurityStamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)]
|
||||||
|
[BitAutoData(KdfType.Argon2id, 4, 65, 5)]
|
||||||
|
public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOff_SuccessLogout(KdfType kdf,
|
||||||
|
int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||||
|
{
|
||||||
|
var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
Assert.NotNull(userBeforeKdfChange);
|
||||||
|
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(false);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var kdfRequest = new KdfRequestModel
|
||||||
|
{
|
||||||
|
KdfType = kdf,
|
||||||
|
Iterations = kdfIterations,
|
||||||
|
Memory = kdfMemory,
|
||||||
|
Parallelism = kdfParallelism,
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfWithKdfRequestAsync(kdfRequest);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
// Validate that the user fields were updated correctly
|
||||||
|
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(kdfRequest.KdfType, user.Kdf);
|
||||||
|
Assert.Equal(kdfRequest.Iterations, user.KdfIterations);
|
||||||
|
Assert.Equal(kdfRequest.Memory, user.KdfMemory);
|
||||||
|
Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(_masterKeyWrappedUserKey, user.Key);
|
||||||
|
Assert.NotNull(user.LastKdfChangeDate);
|
||||||
|
Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.NotEqual(userBeforeKdfChange.SecurityStamp, user.SecurityStamp);
|
||||||
|
Assert.Equal(PasswordVerificationResult.Success,
|
||||||
|
_passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash));
|
||||||
|
|
||||||
|
// Validate push notification
|
||||||
|
await _pushNotificationService.Received(1).PushLogOutAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)]
|
||||||
|
[BitAutoData(KdfType.Argon2id, 4, 65, 5)]
|
||||||
|
public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOn_SuccessSyncAndLogoutWithReason(KdfType kdf,
|
||||||
|
int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||||
|
{
|
||||||
|
var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
Assert.NotNull(userBeforeKdfChange);
|
||||||
|
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(true);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var kdfRequest = new KdfRequestModel
|
||||||
|
{
|
||||||
|
KdfType = kdf,
|
||||||
|
Iterations = kdfIterations,
|
||||||
|
Memory = kdfMemory,
|
||||||
|
Parallelism = kdfParallelism,
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfWithKdfRequestAsync(kdfRequest);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
// Validate that the user fields were updated correctly
|
||||||
|
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(kdfRequest.KdfType, user.Kdf);
|
||||||
|
Assert.Equal(kdfRequest.Iterations, user.KdfIterations);
|
||||||
|
Assert.Equal(kdfRequest.Memory, user.KdfMemory);
|
||||||
|
Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(_masterKeyWrappedUserKey, user.Key);
|
||||||
|
Assert.NotNull(user.LastKdfChangeDate);
|
||||||
|
Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1));
|
||||||
|
Assert.Equal(userBeforeKdfChange.SecurityStamp, user.SecurityStamp);
|
||||||
|
Assert.Equal(PasswordVerificationResult.Success,
|
||||||
|
_passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash));
|
||||||
|
|
||||||
|
// Validate push notification
|
||||||
|
await _pushNotificationService.Received(1)
|
||||||
|
.PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange);
|
||||||
|
await _pushNotificationService.Received(1).PushSyncSettingsAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_Unauthorized_ReturnsUnauthorized()
|
||||||
|
{
|
||||||
|
// Don't call LoginAsync to test unauthorized access
|
||||||
|
|
||||||
|
var response = await PostKdfWithKdfRequestAsync(_defaultKdfRequest);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
[InlineData(true, false)]
|
||||||
|
[InlineData(true, true)]
|
||||||
|
public async Task PostKdf_AuthenticationDataOrUnlockDataNull_BadRequest(bool authenticationDataNull,
|
||||||
|
bool unlockDataNull)
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = authenticationDataNull
|
||||||
|
? null
|
||||||
|
: new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = unlockDataNull
|
||||||
|
? null
|
||||||
|
: new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfAsync(authenticationData, unlockData);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("AuthenticationData and UnlockData must be provided.", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_InvalidMasterPasswordHash_BadRequest()
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestModel = new PasswordRequestModel
|
||||||
|
{
|
||||||
|
MasterPasswordHash = "wrong-master-password-hash",
|
||||||
|
NewMasterPasswordHash = _newMasterPasswordHash,
|
||||||
|
Key = _masterKeyWrappedUserKey,
|
||||||
|
AuthenticationData = authenticationData,
|
||||||
|
UnlockData = unlockData
|
||||||
|
};
|
||||||
|
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf");
|
||||||
|
message.Content = JsonContent.Create(requestModel);
|
||||||
|
var response = await _client.SendAsync(message);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Incorrect password", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_ChangedSaltInAuthenticationData_BadRequest()
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = "wrong-salt@bitwarden.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfAsync(authenticationData, unlockData);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Invalid master password salt.", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_ChangedSaltInUnlockData_BadRequest()
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = "wrong-salt@bitwarden.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfAsync(authenticationData, unlockData);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Invalid master password salt.", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_KdfNotMatching_BadRequest()
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_001 },
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfAsync(authenticationData, unlockData);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("KDF settings must be equal for authentication and unlock.", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(KdfType.PBKDF2_SHA256, 1, null, null)]
|
||||||
|
[InlineData(KdfType.Argon2id, 4, null, 5)]
|
||||||
|
[InlineData(KdfType.Argon2id, 4, 65, null)]
|
||||||
|
public async Task PostKdf_InvalidKdf_BadRequest(KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var kdfRequest = new KdfRequestModel
|
||||||
|
{
|
||||||
|
KdfType = kdf,
|
||||||
|
Iterations = kdfIterations,
|
||||||
|
Memory = kdfMemory,
|
||||||
|
Parallelism = kdfParallelism
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await PostKdfWithKdfRequestAsync(kdfRequest);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("KDF settings are invalid", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostKdf_InvalidNewMasterPassword_BadRequest()
|
||||||
|
{
|
||||||
|
var newMasterPasswordHash = "too-short";
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = _defaultKdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestModel = new PasswordRequestModel
|
||||||
|
{
|
||||||
|
MasterPasswordHash = _masterPasswordHash,
|
||||||
|
NewMasterPasswordHash = newMasterPasswordHash,
|
||||||
|
Key = _masterKeyWrappedUserKey,
|
||||||
|
AuthenticationData = authenticationData,
|
||||||
|
UnlockData = unlockData
|
||||||
|
};
|
||||||
|
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf");
|
||||||
|
message.Content = JsonContent.Create(requestModel);
|
||||||
|
var response = await _client.SendAsync(message);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Passwords must be at least", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostKdfWithKdfRequestAsync(KdfRequestModel kdfRequest)
|
||||||
|
{
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = kdfRequest,
|
||||||
|
MasterPasswordAuthenticationHash = _newMasterPasswordHash,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var unlockData = new MasterPasswordUnlockDataRequestModel
|
||||||
|
{
|
||||||
|
Kdf = kdfRequest,
|
||||||
|
MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,
|
||||||
|
Salt = _ownerEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
return await PostKdfAsync(authenticationData, unlockData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostKdfAsync(
|
||||||
|
MasterPasswordAuthenticationDataRequestModel? authenticationDataRequest,
|
||||||
|
MasterPasswordUnlockDataRequestModel? unlockDataRequest)
|
||||||
|
{
|
||||||
|
var requestModel = new PasswordRequestModel
|
||||||
|
{
|
||||||
|
MasterPasswordHash = _masterPasswordHash,
|
||||||
|
NewMasterPasswordHash = _newMasterPasswordHash,
|
||||||
|
Key = _masterKeyWrappedUserKey,
|
||||||
|
AuthenticationData = authenticationDataRequest,
|
||||||
|
UnlockData = unlockDataRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf");
|
||||||
|
message.Content = JsonContent.Create(requestModel);
|
||||||
|
return await _client.SendAsync(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Kdf;
|
using Bit.Core.KeyManagement.Kdf;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -617,6 +618,16 @@ public class AccountsControllerTests : IDisposable
|
|||||||
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
|
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model)
|
||||||
|
{
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKdf(model));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
|
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
|
||||||
@ -626,7 +637,9 @@ public class AccountsControllerTests : IDisposable
|
|||||||
model.AuthenticationData = null;
|
model.AuthenticationData = null;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||||
|
|
||||||
|
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -638,7 +651,41 @@ public class AccountsControllerTests : IDisposable
|
|||||||
model.UnlockData = null;
|
model.UnlockData = null;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||||
|
|
||||||
|
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostKdf_ChangeKdfFailed_ShouldFail(
|
||||||
|
User user, PasswordRequestModel model)
|
||||||
|
{
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||||
|
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" })));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||||
|
|
||||||
|
Assert.NotNull(exception.ModelState);
|
||||||
|
Assert.Contains("Change KDF failed",
|
||||||
|
exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostKdf_ChangeKdfSuccess_NoError(
|
||||||
|
User user, PasswordRequestModel model)
|
||||||
|
{
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||||
|
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.PostKdf(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Below are helper functions that currently belong to this
|
// Below are helper functions that currently belong to this
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Kdf.Implementations;
|
using Bit.Core.KeyManagement.Kdf.Implementations;
|
||||||
using Bit.Core.KeyManagement.Models.Data;
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -21,16 +23,12 @@ public class ChangeKdfCommandTests
|
|||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
|
||||||
var kdf = new KdfSettings
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
|
||||||
{
|
|
||||||
KdfType = Enums.KdfType.Argon2id,
|
|
||||||
Iterations = 4,
|
|
||||||
Memory = 512,
|
|
||||||
Parallelism = 4
|
|
||||||
};
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
Kdf = kdf,
|
Kdf = kdf,
|
||||||
@ -59,13 +57,7 @@ public class ChangeKdfCommandTests
|
|||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)
|
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var kdf = new KdfSettings
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
|
||||||
{
|
|
||||||
KdfType = Enums.KdfType.Argon2id,
|
|
||||||
Iterations = 4,
|
|
||||||
Memory = 512,
|
|
||||||
Parallelism = 4
|
|
||||||
};
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
Kdf = kdf,
|
Kdf = kdf,
|
||||||
@ -85,17 +77,13 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider,
|
||||||
|
User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(false));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
var kdf = new KdfSettings
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
|
||||||
{
|
|
||||||
KdfType = Enums.KdfType.Argon2id,
|
|
||||||
Iterations = 4,
|
|
||||||
Memory = 512,
|
|
||||||
Parallelism = 4
|
|
||||||
};
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
Kdf = kdf,
|
Kdf = kdf,
|
||||||
@ -116,7 +104,9 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task
|
||||||
|
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut(
|
||||||
|
SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
{
|
{
|
||||||
var constantKdf = new KdfSettings
|
var constantKdf = new KdfSettings
|
||||||
{
|
{
|
||||||
@ -137,8 +127,12 @@ public class ChangeKdfCommandTests
|
|||||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||||
Salt = user.GetMasterPasswordSalt()
|
Salt = user.GetMasterPasswordSalt()
|
||||||
};
|
};
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||||
|
|
||||||
@ -150,17 +144,79 @@ public class ChangeKdfCommandTests
|
|||||||
&& u.KdfParallelism == constantKdf.Parallelism
|
&& u.KdfParallelism == constantKdf.Parallelism
|
||||||
&& u.Key == "new-wrapped-key"
|
&& u.Key == "new-wrapped-key"
|
||||||
));
|
));
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,
|
||||||
|
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true);
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task
|
||||||
|
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut(
|
||||||
|
SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
var constantKdf = new KdfSettings
|
||||||
|
{
|
||||||
|
KdfType = Enums.KdfType.Argon2id,
|
||||||
|
Iterations = 5,
|
||||||
|
Memory = 1024,
|
||||||
|
Parallelism = 4
|
||||||
|
};
|
||||||
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
|
{
|
||||||
|
Kdf = constantKdf,
|
||||||
|
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||||
|
Salt = user.GetMasterPasswordSalt()
|
||||||
|
};
|
||||||
|
var unlockData = new MasterPasswordUnlockData
|
||||||
|
{
|
||||||
|
Kdf = constantKdf,
|
||||||
|
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||||
|
Salt = user.GetMasterPasswordSalt()
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
|
||||||
|
u.Id == user.Id
|
||||||
|
&& u.Kdf == constantKdf.KdfType
|
||||||
|
&& u.KdfIterations == constantKdf.Iterations
|
||||||
|
&& u.KdfMemory == constantKdf.Memory
|
||||||
|
&& u.KdfParallelism == constantKdf.Parallelism
|
||||||
|
&& u.Key == "new-wrapped-key"
|
||||||
|
));
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,
|
||||||
|
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false);
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||||
|
.PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange);
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncSettingsAsync(user.Id);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(
|
||||||
|
SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
Kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 },
|
Kdf = new KdfSettings
|
||||||
|
{
|
||||||
|
KdfType = Enums.KdfType.Argon2id,
|
||||||
|
Iterations = 4,
|
||||||
|
Memory = 512,
|
||||||
|
Parallelism = 4
|
||||||
|
},
|
||||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||||
Salt = user.GetMasterPasswordSalt()
|
Salt = user.GetMasterPasswordSalt()
|
||||||
};
|
};
|
||||||
@ -176,9 +232,11 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
|
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,
|
||||||
|
KdfSettings kdf)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
@ -192,15 +250,17 @@ public class ChangeKdfCommandTests
|
|||||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||||
Salt = user.GetMasterPasswordSalt()
|
Salt = user.GetMasterPasswordSalt()
|
||||||
};
|
};
|
||||||
await Assert.ThrowsAsync<ArgumentException>(async () =>
|
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
|
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,
|
||||||
|
KdfSettings kdf)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
@ -214,25 +274,22 @@ public class ChangeKdfCommandTests
|
|||||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||||
Salt = "different-salt"
|
Salt = "different-salt"
|
||||||
};
|
};
|
||||||
await Assert.ThrowsAsync<ArgumentException>(async () =>
|
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider,
|
||||||
|
User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" });
|
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" });
|
||||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(failedResult));
|
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(failedResult));
|
||||||
|
|
||||||
var kdf = new KdfSettings
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
|
||||||
{
|
|
||||||
KdfType = Enums.KdfType.Argon2id,
|
|
||||||
Iterations = 4,
|
|
||||||
Memory = 512,
|
|
||||||
Parallelism = 4
|
|
||||||
};
|
|
||||||
var authenticationData = new MasterPasswordAuthenticationData
|
var authenticationData = new MasterPasswordAuthenticationData
|
||||||
{
|
{
|
||||||
Kdf = kdf,
|
Kdf = kdf,
|
||||||
@ -253,9 +310,11 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(
|
||||||
|
SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// Create invalid KDF settings (iterations too low for PBKDF2)
|
// Create invalid KDF settings (iterations too low for PBKDF2)
|
||||||
var invalidKdf = new KdfSettings
|
var invalidKdf = new KdfSettings
|
||||||
@ -287,9 +346,11 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(
|
||||||
|
SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// Create invalid Argon2 KDF settings (memory too high)
|
// Create invalid Argon2 KDF settings (memory too high)
|
||||||
var invalidKdf = new KdfSettings
|
var invalidKdf = new KdfSettings
|
||||||
@ -318,5 +379,4 @@ public class ChangeKdfCommandTests
|
|||||||
|
|
||||||
Assert.Equal("KDF settings are invalid.", exception.Message);
|
Assert.Equal("KDF settings are invalid.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -358,20 +358,28 @@ public class AzureQueuePushEngineTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true)]
|
[InlineData(true, null)]
|
||||||
[InlineData(false)]
|
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
|
||||||
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext)
|
[InlineData(false, null)]
|
||||||
|
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
|
||||||
|
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,
|
||||||
|
PushNotificationLogOutReason? reason)
|
||||||
{
|
{
|
||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var payload = new JsonObject
|
||||||
|
{
|
||||||
|
["UserId"] = userId
|
||||||
|
};
|
||||||
|
if (reason != null)
|
||||||
|
{
|
||||||
|
payload["Reason"] = (int)reason;
|
||||||
|
}
|
||||||
|
|
||||||
var expectedPayload = new JsonObject
|
var expectedPayload = new JsonObject
|
||||||
{
|
{
|
||||||
["Type"] = 11,
|
["Type"] = 11,
|
||||||
["Payload"] = new JsonObject
|
["Payload"] = payload,
|
||||||
{
|
|
||||||
["UserId"] = userId,
|
|
||||||
["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (excludeCurrentContext)
|
if (excludeCurrentContext)
|
||||||
@ -380,7 +388,7 @@ public class AzureQueuePushEngineTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
await VerifyNotificationAsync(
|
await VerifyNotificationAsync(
|
||||||
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
|
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
|
||||||
expectedPayload
|
expectedPayload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.Platform.Push.Internal;
|
using Bit.Core.Platform.Push.Internal;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
@ -193,7 +194,8 @@ public class NotificationsApiPushEngineTests : PushTestBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext)
|
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
|
||||||
|
PushNotificationLogOutReason? reason)
|
||||||
{
|
{
|
||||||
JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null;
|
JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null;
|
||||||
|
|
||||||
@ -203,7 +205,7 @@ public class NotificationsApiPushEngineTests : PushTestBase
|
|||||||
["Payload"] = new JsonObject
|
["Payload"] = new JsonObject
|
||||||
{
|
{
|
||||||
["UserId"] = userId,
|
["UserId"] = userId,
|
||||||
["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime,
|
["Reason"] = reason != null ? (int)reason : null
|
||||||
},
|
},
|
||||||
["ContextId"] = contextId,
|
["ContextId"] = contextId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -86,7 +86,8 @@ public abstract class PushTestBase
|
|||||||
protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId);
|
protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId);
|
||||||
protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId);
|
protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId);
|
||||||
protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId);
|
protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId);
|
||||||
protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext);
|
protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
|
||||||
|
PushNotificationLogOutReason? reason);
|
||||||
protected abstract JsonNode GetPushSendCreatePayload(Send send);
|
protected abstract JsonNode GetPushSendCreatePayload(Send send);
|
||||||
protected abstract JsonNode GetPushSendUpdatePayload(Send send);
|
protected abstract JsonNode GetPushSendUpdatePayload(Send send);
|
||||||
protected abstract JsonNode GetPushSendDeletePayload(Send send);
|
protected abstract JsonNode GetPushSendDeletePayload(Send send);
|
||||||
@ -263,15 +264,18 @@ public abstract class PushTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true)]
|
[InlineData(true, null)]
|
||||||
[InlineData(false)]
|
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
|
||||||
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext)
|
[InlineData(false, null)]
|
||||||
|
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
|
||||||
|
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,
|
||||||
|
PushNotificationLogOutReason? reason)
|
||||||
{
|
{
|
||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
await VerifyNotificationAsync(
|
await VerifyNotificationAsync(
|
||||||
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
|
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
|
||||||
GetPushLogOutPayload(userId, excludeCurrentContext)
|
GetPushLogOutPayload(userId, excludeCurrentContext, reason)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Text.Json.Nodes;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.Platform.Push.Internal;
|
using Bit.Core.Platform.Push.Internal;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -274,7 +275,8 @@ public class RelayPushNotificationServiceTests : PushTestBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext)
|
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
|
||||||
|
PushNotificationLogOutReason? reason)
|
||||||
{
|
{
|
||||||
JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null;
|
JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null;
|
||||||
|
|
||||||
@ -288,7 +290,7 @@ public class RelayPushNotificationServiceTests : PushTestBase
|
|||||||
["Payload"] = new JsonObject
|
["Payload"] = new JsonObject
|
||||||
{
|
{
|
||||||
["UserId"] = userId,
|
["UserId"] = userId,
|
||||||
["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime,
|
["Reason"] = reason != null ? (int)reason : null
|
||||||
},
|
},
|
||||||
["ClientType"] = null,
|
["ClientType"] = null,
|
||||||
["InstallationId"] = null,
|
["InstallationId"] = null,
|
||||||
|
|||||||
@ -404,16 +404,18 @@ public class NotificationHubPushNotificationServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true)]
|
[InlineData(true, null)]
|
||||||
[InlineData(false)]
|
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
|
||||||
public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext)
|
[InlineData(false, null)]
|
||||||
|
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
|
||||||
|
public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext, PushNotificationLogOutReason? reason)
|
||||||
{
|
{
|
||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
var expectedPayload = new JsonObject
|
var expectedPayload = new JsonObject
|
||||||
{
|
{
|
||||||
["UserId"] = userId,
|
["UserId"] = userId,
|
||||||
["Date"] = _now,
|
["Reason"] = reason != null ? (int)reason : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
var expectedTag = excludeCurrentContext
|
var expectedTag = excludeCurrentContext
|
||||||
@ -421,7 +423,7 @@ public class NotificationHubPushNotificationServiceTests
|
|||||||
: $"(template:payload_userId:{userId})";
|
: $"(template:payload_userId:{userId})";
|
||||||
|
|
||||||
await VerifyNotificationAsync(
|
await VerifyNotificationAsync(
|
||||||
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
|
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
|
||||||
PushType.LogOut,
|
PushType.LogOut,
|
||||||
expectedPayload,
|
expectedPayload,
|
||||||
expectedTag
|
expectedTag
|
||||||
|
|||||||
@ -452,7 +452,8 @@ public class ProfileServiceTests
|
|||||||
user.SecurityStamp = securityStamp;
|
user.SecurityStamp = securityStamp;
|
||||||
|
|
||||||
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
||||||
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, securityStamp)
|
new Claim("sub", user.Id.ToString()),
|
||||||
|
new Claim(Claims.SecurityStamp, securityStamp)
|
||||||
]));
|
]));
|
||||||
|
|
||||||
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
||||||
@ -486,7 +487,8 @@ public class ProfileServiceTests
|
|||||||
user.SecurityStamp = "current-security-stamp";
|
user.SecurityStamp = "current-security-stamp";
|
||||||
|
|
||||||
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
||||||
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, "old-security-stamp")
|
new Claim("sub", user.Id.ToString()),
|
||||||
|
new Claim(Claims.SecurityStamp, "old-security-stamp")
|
||||||
]));
|
]));
|
||||||
|
|
||||||
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
||||||
@ -517,7 +519,8 @@ public class ProfileServiceTests
|
|||||||
user.SecurityStamp = "current-stamp";
|
user.SecurityStamp = "current-stamp";
|
||||||
|
|
||||||
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
||||||
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, claimStamp)
|
new Claim("sub", user.Id.ToString()),
|
||||||
|
new Claim(Claims.SecurityStamp, claimStamp)
|
||||||
]));
|
]));
|
||||||
|
|
||||||
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
||||||
@ -546,7 +549,8 @@ public class ProfileServiceTests
|
|||||||
{
|
{
|
||||||
context.Client.ClientId = client;
|
context.Client.ClientId = client;
|
||||||
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
|
||||||
new Claim("sub", user.Id.ToString()), new Claim("email", user.Email)
|
new Claim("sub", user.Id.ToString()),
|
||||||
|
new Claim("email", user.Email)
|
||||||
]));
|
]));
|
||||||
|
|
||||||
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user