diff --git a/src/Core/Enums/PushNotificationLogOutReason.cs b/src/Core/Enums/PushNotificationLogOutReason.cs new file mode 100644 index 0000000000..a24f790305 --- /dev/null +++ b/src/Core/Enums/PushNotificationLogOutReason.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Enums; + +public enum PushNotificationLogOutReason : byte +{ + KdfChange = 0 +} diff --git a/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs index fe736f9ac6..83e47c4931 100644 --- a/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs +++ b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs @@ -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 _logger; + private readonly IFeatureService _featureService; - public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, IUserRepository userRepository, IdentityErrorDescriber describer, ILogger logger) + public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, + IUserRepository userRepository, IdentityErrorDescriber describer, ILogger logger, + IFeatureService featureService) { _userService = userService; _pushService = pushService; _userRepository = userRepository; _identityErrorDescriber = describer; _logger = logger; + _featureService = featureService; } - public async Task ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData) + public async Task 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; } } diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs index c0ae949a3f..1bc7006cef 100644 --- a/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs @@ -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."); } } } diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs index d1ab6f645b..cb18ed2a78 100644 --- a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs @@ -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."); } } } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index c4ae1e2858..a622b98e05 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -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; } +} diff --git a/src/Core/Platform/Push/IPushNotificationService.cs b/src/Core/Platform/Push/IPushNotificationService.cs index 32a488b827..b6d7d4d416 100644 --- a/src/Core/Platform/Push/IPushNotificationService.cs +++ b/src/Core/Platform/Push/IPushNotificationService.cs @@ -167,18 +167,17 @@ public interface IPushNotificationService ExcludeCurrentContext = false, }); - Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false) - => PushAsync(new PushNotification + Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false, + PushNotificationLogOutReason? reason = null) + => PushAsync(new PushNotification { 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, }); diff --git a/src/Core/Platform/Push/PushType.cs b/src/Core/Platform/Push/PushType.cs index 7765c1aa66..93eca86243 100644 --- a/src/Core/Platform/Push/PushType.cs +++ b/src/Core/Platform/Push/PushType.cs @@ -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))] diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 69d5bdc958..0fea72edc3 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -64,7 +64,7 @@ public static class HubHelpers case PushType.SyncSettings: case PushType.LogOut: var userNotification = - JsonSerializer.Deserialize>( + JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); await hubContext.Clients.User(userNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, userNotification, cancellationToken); diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs index 4e5a6850e7..09ec5b010f 100644 --- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs @@ -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.Helpers; +using Bit.Api.KeyManagement.Models.Requests; 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; namespace Bit.Api.IntegrationTest.Controllers; -public class AccountsControllerTest : IClassFixture +public class AccountsControllerTest : IClassFixture, 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 _passwordHasher; + + private string _ownerEmail = null!; + + public AccountsControllerTest(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService(_ => { }); + _factory.SubstituteService(_ => { }); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _factory.GetService(); + _pushNotificationService = _factory.GetService(); + _featureService = _factory.GetService(); + _passwordHasher = _factory.GetService>(); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } [Fact] public async Task GetAccountsProfile_success() { - var tokens = await _factory.LoginWithNewAccount(); - var client = _factory.CreateClient(); + await _loginHelper.LoginAsync(_ownerEmail); 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(); var content = await response.Content.ReadFromJsonAsync(); Assert.NotNull(content); - Assert.Equal("integration-test@bitwarden.com", content.Email); + Assert.Equal(_ownerEmail, content.Email); Assert.NotNull(content.Name); Assert.True(content.EmailVerified); Assert.False(content.Premium); @@ -35,4 +85,354 @@ public class AccountsControllerTest : IClassFixture Assert.NotNull(content.PrivateKey); 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 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 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); + } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index e2959a119c..f1aa11d068 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -617,6 +618,16 @@ public class AccountsControllerTests : IDisposable await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user); } + [Theory] + [BitAutoData] + public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(null)); + + // Act + await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + } + [Theory] [BitAutoData] public async Task PostKdf_WithNullAuthenticationData_ShouldFail( @@ -626,7 +637,9 @@ public class AccountsControllerTests : IDisposable model.AuthenticationData = null; // Act - await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + var exception = await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + + Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message); } [Theory] @@ -638,7 +651,41 @@ public class AccountsControllerTests : IDisposable model.UnlockData = null; // Act - await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + var exception = await Assert.ThrowsAsync(() => _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()).Returns(Task.FromResult(user)); + _changeKdfCommand.ChangeKdfAsync(Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" }))); + + // Act + var exception = await Assert.ThrowsAsync(() => _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()).Returns(Task.FromResult(user)); + _changeKdfCommand.ChangeKdfAsync(Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + + // Act + await _sut.PostKdf(model); } // Below are helper functions that currently belong to this diff --git a/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs index 02e04b9ce9..991935b928 100644 --- a/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs +++ b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs @@ -1,9 +1,11 @@ #nullable enable using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Kdf.Implementations; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -21,16 +23,12 @@ public class ChangeKdfCommandTests [BitAutoData] public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -59,13 +57,7 @@ public class ChangeKdfCommandTests [BitAutoData] public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider sutProvider) { - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -85,17 +77,13 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider sutProvider, + User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(false)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(false)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -116,7 +104,9 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider sutProvider, User user) + public async Task + ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut( + SutProvider sutProvider, User user) { var constantKdf = new KdfSettings { @@ -137,8 +127,12 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = user.GetMasterPasswordSalt() }; - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .UpdatePasswordHash(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().IsEnabled(Arg.Any()).Returns(false); await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); @@ -150,17 +144,79 @@ public class ChangeKdfCommandTests && u.KdfParallelism == constantKdf.Parallelism && u.Key == "new-wrapped-key" )); + await sutProvider.GetDependency().Received(1).UpdatePasswordHash(user, + authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true); + await sutProvider.GetDependency().Received(1).PushLogOutAsync(user.Id); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task + ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).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().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .UpdatePasswordHash(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().IsEnabled(Arg.Any()).Returns(true); + + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is(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().Received(1).UpdatePasswordHash(user, + authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false); + await sutProvider.GetDependency().Received(1) + .PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange); + await sutProvider.GetDependency().Received(1).PushSyncSettingsAsync(user.Id); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException( + SutProvider sutProvider, User user) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); 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", Salt = user.GetMasterPasswordSalt() }; @@ -176,9 +232,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider sutProvider, User user, KdfSettings kdf) + public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider sutProvider, User user, + KdfSettings kdf) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var authenticationData = new MasterPasswordAuthenticationData { @@ -192,15 +250,17 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = user.GetMasterPasswordSalt() }; - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider sutProvider, User user, KdfSettings kdf) + public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider sutProvider, User user, + KdfSettings kdf) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var authenticationData = new MasterPasswordAuthenticationData { @@ -214,25 +274,22 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = "different-salt" }; - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider sutProvider, + User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" }); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(failedResult)); + sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(failedResult)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -253,9 +310,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); // Create invalid KDF settings (iterations too low for PBKDF2) var invalidKdf = new KdfSettings @@ -287,9 +346,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); // Create invalid Argon2 KDF settings (memory too high) var invalidKdf = new KdfSettings @@ -318,5 +379,4 @@ public class ChangeKdfCommandTests Assert.Equal("KDF settings are invalid.", exception.Message); } - } diff --git a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs index 9c46211517..3f31f1fad4 100644 --- a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs @@ -358,20 +358,28 @@ public class AzureQueuePushEngineTests } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); + var payload = new JsonObject + { + ["UserId"] = userId + }; + if (reason != null) + { + payload["Reason"] = (int)reason; + } + var expectedPayload = new JsonObject { ["Type"] = 11, - ["Payload"] = new JsonObject - { - ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, - }, + ["Payload"] = payload, }; if (excludeCurrentContext) @@ -380,7 +388,7 @@ public class AzureQueuePushEngineTests } await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), expectedPayload ); } diff --git a/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs index c61c2f37d0..7f230c4e5c 100644 --- a/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs @@ -1,6 +1,7 @@ using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push.Internal; 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; @@ -203,7 +205,7 @@ public class NotificationsApiPushEngineTests : PushTestBase ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + ["Reason"] = reason != null ? (int)reason : null }, ["ContextId"] = contextId, }; diff --git a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs index e0eeeda97d..c0037f57aa 100644 --- a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs @@ -86,7 +86,8 @@ public abstract class PushTestBase protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId); protected abstract JsonNode GetPushSyncOrgKeysPayload(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 GetPushSendUpdatePayload(Send send); protected abstract JsonNode GetPushSendDeletePayload(Send send); @@ -263,15 +264,18 @@ public abstract class PushTestBase } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), - GetPushLogOutPayload(userId, excludeCurrentContext) + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), + GetPushLogOutPayload(userId, excludeCurrentContext, reason) ); } diff --git a/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs index 010ad40d13..f8ae07f647 100644 --- a/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs @@ -4,6 +4,7 @@ using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; @@ -64,7 +65,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["UserId"] = cipher.UserId, ["OrganizationId"] = null, // Currently CollectionIds are not passed along from the method signature - // to the request body. + // to the request body. ["CollectionIds"] = null, ["RevisionDate"] = cipher.RevisionDate, }, @@ -88,7 +89,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["UserId"] = cipher.UserId, ["OrganizationId"] = null, // Currently CollectionIds are not passed along from the method signature - // to the request body. + // to the request body. ["CollectionIds"] = null, ["RevisionDate"] = cipher.RevisionDate, }, @@ -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; @@ -288,7 +290,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + ["Reason"] = reason != null ? (int)reason : null }, ["ClientType"] = null, ["InstallationId"] = null, diff --git a/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs index a32b112675..f5f257c741 100644 --- a/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs @@ -404,16 +404,18 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext, PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); var expectedPayload = new JsonObject { ["UserId"] = userId, - ["Date"] = _now, + ["Reason"] = reason != null ? (int)reason : null, }; var expectedTag = excludeCurrentContext @@ -421,7 +423,7 @@ public class NotificationHubPushNotificationServiceTests : $"(template:payload_userId:{userId})"; await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), PushType.LogOut, expectedPayload, expectedTag diff --git a/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs index 3e6b7e931b..aaf8b269d6 100644 --- a/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs +++ b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs @@ -15,8 +15,8 @@ internal class ProfileDataRequestContextCustomization : ICustomization fixture.Customize(composer => composer .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ new Claim("sub", Guid.NewGuid().ToString()), - new Claim("name", "Test User"), - new Claim("email", "test@example.com") + new Claim("name", "Test User"), + new Claim("email", "test@example.com") ]))) .With(o => o.Client, new Client { ClientId = "web" }) .With(o => o.ValidatedRequest, () => null) @@ -41,7 +41,7 @@ internal class IsActiveContextCustomization : ICustomization fixture.Customize(composer => composer .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ new Claim("sub", Guid.NewGuid().ToString()), - new Claim(Claims.SecurityStamp, "test-security-stamp") + new Claim(Claims.SecurityStamp, "test-security-stamp") ]))) .With(o => o.Client, new Client { ClientId = "web" }) .With(o => o.IsActive, false) diff --git a/test/Identity.Test/IdentityServer/ProfileServiceTests.cs b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs index c20a240370..c467f074ac 100644 --- a/test/Identity.Test/IdentityServer/ProfileServiceTests.cs +++ b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs @@ -452,7 +452,8 @@ public class ProfileServiceTests user.SecurityStamp = securityStamp; 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); @@ -486,7 +487,8 @@ public class ProfileServiceTests user.SecurityStamp = "current-security-stamp"; 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); @@ -517,7 +519,8 @@ public class ProfileServiceTests user.SecurityStamp = "current-stamp"; 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); @@ -546,7 +549,8 @@ public class ProfileServiceTests { context.Client.ClientId = client; 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);