From b072fc56b13a7613586c608852fe3d8e3640c40a Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Mon, 3 Jun 2024 09:19:56 -0400 Subject: [PATCH] [PM-6794] block legacy users from authN (#4088) * block legacy users from authN * undo change to GetDeviceFromRequest * lint * add feature flag * format * add web vault url to error message * fix test * format --- src/Core/Constants.cs | 1 + src/Core/Services/IUserService.cs | 6 +++ .../Services/Implementations/UserService.cs | 22 +++++++++ .../IdentityServer/BaseRequestValidator.cs | 18 +++++++ .../CustomTokenRequestValidator.cs | 13 +++++ .../Endpoints/IdentityServerTests.cs | 47 +++++++++++++++++++ 6 files changed, 107 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d2621db4c..111c00ce6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -129,6 +129,7 @@ public static class FeatureFlagKeys public const string VaultBulkManagementAction = "vault-bulk-management-action"; public const string BulkDeviceApproval = "bulk-device-approval"; public const string MemberAccessReport = "ac-2059-member-access-report"; + public const string BlockLegacyUsers = "block-legacy-users"; public static List GetAllKeys() { diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index f4751623a..6cdc4fc6b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -76,4 +76,10 @@ public interface IUserService Task SendOTPAsync(User user); Task VerifyOTPAsync(User user, string token); Task VerifySecretAsync(User user, string secret); + + /// + /// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key. + /// We force these users to the web to migrate their encryption scheme. + /// + Task IsLegacyUser(string userId); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 148c60a14..48e92576c 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1304,6 +1304,28 @@ public class UserService : UserManager, IUserService, IDisposable return IdentityResult.Success; } + public async Task IsLegacyUser(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + { + return false; + } + + var user = await FindByIdAsync(userId); + if (user == null) + { + return false; + } + + return IsLegacyUser(user); + } + + /// + public static bool IsLegacyUser(User user) + { + return user.Key == null && user.MasterPassword != null && user.PrivateKey != null; + } + private async Task ValidatePasswordInternal(User user, string password) { var errors = new List(); diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 406fd5bf7..e39187c82 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -162,6 +162,17 @@ public abstract class BaseRequestValidator where T : class twoFactorToken = null; } + + // Force legacy users to the web for migration + if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) + { + if (UserService.IsLegacyUser(user) && request.ClientId != "web") + { + await FailAuthForLegacyUserAsync(user, context); + return; + } + } + // Returns true if can finish validation process if (await IsValidAuthTypeAsync(user, request.GrantType)) { @@ -184,6 +195,13 @@ public abstract class BaseRequestValidator where T : class } } + protected async Task FailAuthForLegacyUserAsync(User user, T context) + { + await BuildErrorResultAsync( + $"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}", + false, context, user); + } + protected abstract Task ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext); protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken) diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index b69f4dacb..45024075c 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -13,6 +13,7 @@ using Bit.Core.Settings; using Bit.Core.Tokens; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; +using HandlebarsDotNet; using IdentityModel; using Microsoft.AspNetCore.Identity; @@ -57,6 +58,17 @@ public class CustomTokenRequestValidator : BaseRequestValidator { { "encrypted_payload", payload } }; } + return; } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index 5717bc6d5..a7f09cfe0 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -331,6 +331,53 @@ public class IdentityServerTests : IClassFixture await AssertDefaultTokenBodyAsync(context, "api"); } + [Theory, BitAutoData] + public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(string deviceId) + { + var server = _factory.WithWebHostBuilder(builder => + { + builder.UseSetting("globalSettings:launchDarkly:flagValues:block-legacy-users", "true"); + }).Server; + + var username = "test+tokenclientcredentials@email.com"; + + + await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel + { + Email = username, + MasterPasswordHash = "master_password_hash" + })); + + + var database = _factory.GetDatabaseContext(); + var user = await database.Users + .FirstAsync(u => u.Email == username); + + user.PrivateKey = "EncryptedPrivateKey"; + await database.SaveChangesAsync(); + + var context = await server.PostAsync("/connect/token", new FormUrlEncodedContent( + new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "browser" }, + { "deviceType", DeviceTypeAsString(DeviceType.ChromeBrowser) }, + { "deviceIdentifier", deviceId }, + { "deviceName", "chrome" }, + { "grant_type", "password" }, + { "username", username }, + { "password", "master_password_hash" }, + }), context => context.SetAuthEmail(username)); + + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + + var errorBody = await AssertHelper.AssertResponseTypeIs(context); + var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "ErrorModel", JsonValueKind.Object); + var message = AssertHelper.AssertJsonProperty(error, "Message", JsonValueKind.String).GetString(); + Assert.StartsWith("Encryption key migration is required.", message); + } + + [Theory, BitAutoData] public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_Success(Organization organization, Bit.Core.Entities.OrganizationApiKey organizationApiKey) {