diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 847214598c..707001ddcc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,6 +115,7 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; + public const string BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs index 34a9d6c573..61543f9751 100644 --- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs @@ -22,6 +22,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder private readonly ICurrentContext _currentContext; private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private UserDecryptionOptions _options = new UserDecryptionOptions(); private User? _user; @@ -31,12 +32,14 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder public UserDecryptionOptionsBuilder( ICurrentContext currentContext, IDeviceRepository deviceRepository, - IOrganizationUserRepository organizationUserRepository + IOrganizationUserRepository organizationUserRepository, + ILoginApprovingClientTypes loginApprovingClientTypes ) { _currentContext = currentContext; _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; + _loginApprovingClientTypes = loginApprovingClientTypes; } public IUserDecryptionOptionsBuilder ForUser(User user) @@ -119,8 +122,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder // Checks if the current user has any devices that are capable of approving login with device requests except for // their current device. // NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting. - hasLoginApprovingDevice = allDevices - .Any(d => d.Identifier != _device.Identifier && LoginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); + hasLoginApprovingDevice = allDevices.Any(d => d.Identifier != _device.Identifier && _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); } // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP diff --git a/src/Identity/Utilities/LoginApprovingClientTypes.cs b/src/Identity/Utilities/LoginApprovingClientTypes.cs index dd27a87550..f0c7b831b7 100644 --- a/src/Identity/Utilities/LoginApprovingClientTypes.cs +++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs @@ -1,22 +1,39 @@ -using Bit.Core.Enums; +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Services; namespace Bit.Identity.Utilities; -public static class LoginApprovingClientTypes +public interface ILoginApprovingClientTypes { - private static readonly IReadOnlyCollection _clientTypesThatCanApprove; + IReadOnlyCollection TypesThatCanApprove { get; } +} - static LoginApprovingClientTypes() +public class LoginApprovingClientTypes : ILoginApprovingClientTypes +{ + public LoginApprovingClientTypes( + IFeatureService featureService) { - var clientTypes = new List + if (featureService.IsEnabled(FeatureFlagKeys.BrowserExtensionLoginApproval)) { - ClientType.Desktop, - ClientType.Mobile, - ClientType.Web, - ClientType.Browser, - }; - _clientTypesThatCanApprove = clientTypes.AsReadOnly(); + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }; + } + else + { + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }; + } } - public static IReadOnlyCollection TypesThatCanApprove => _clientTypesThatCanApprove; + public IReadOnlyCollection TypesThatCanApprove { get; } } diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 36c38615a2..bf90b1aa24 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index 0a6346fe9e..25182743e5 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Identity.IdentityServer; +using Bit.Identity.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; @@ -17,6 +18,7 @@ public class UserDecryptionOptionsBuilderTests private readonly ICurrentContext _currentContext; private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private readonly UserDecryptionOptionsBuilder _builder; public UserDecryptionOptionsBuilderTests() @@ -24,7 +26,8 @@ public class UserDecryptionOptionsBuilderTests _currentContext = Substitute.For(); _deviceRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); - _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository); + _loginApprovingClientTypes = Substitute.For(); + _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes); } [Theory] @@ -120,17 +123,17 @@ public class UserDecryptionOptionsBuilderTests [BitAutoData(DeviceType.SafariBrowser)] [BitAutoData(DeviceType.VivaldiBrowser)] [BitAutoData(DeviceType.UnknownBrowser)] - // Extension - [BitAutoData(DeviceType.ChromeExtension)] - [BitAutoData(DeviceType.FirefoxExtension)] - [BitAutoData(DeviceType.OperaExtension)] - [BitAutoData(DeviceType.EdgeExtension)] - [BitAutoData(DeviceType.VivaldiExtension)] - [BitAutoData(DeviceType.SafariExtension)] public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue( DeviceType deviceType, SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }); + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; ssoConfig.Data = configurationData.Serialize(); approvingDevice.Type = deviceType; @@ -142,9 +145,65 @@ public class UserDecryptionOptionsBuilderTests } [Theory] + // Desktop + [BitAutoData(DeviceType.LinuxDesktop)] + [BitAutoData(DeviceType.MacOsDesktop)] + [BitAutoData(DeviceType.WindowsDesktop)] + [BitAutoData(DeviceType.UWP)] + // Mobile + [BitAutoData(DeviceType.Android)] + [BitAutoData(DeviceType.iOS)] + [BitAutoData(DeviceType.AndroidAmazon)] + // Web + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.FirefoxBrowser)] + [BitAutoData(DeviceType.OperaBrowser)] + [BitAutoData(DeviceType.EdgeBrowser)] + [BitAutoData(DeviceType.IEBrowser)] + [BitAutoData(DeviceType.SafariBrowser)] + [BitAutoData(DeviceType.VivaldiBrowser)] + [BitAutoData(DeviceType.UnknownBrowser)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] + public async Task Build_WhenHasLoginApprovingDeviceFeatureFlag_ShouldApprovingDeviceTrue( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }); + + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + approvingDevice.Type = deviceType; + _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); + + Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice); + } + + [Theory] + // CLI [BitAutoData(DeviceType.WindowsCLI)] [BitAutoData(DeviceType.MacOsCLI)] [BitAutoData(DeviceType.LinuxCLI)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse( DeviceType deviceType, SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)