Repository: bitwarden/server
Pull request #5792: Auth/pm 17111/add browser to list of approving clients
Author: login: Patrick-Pimentel-Bitwarden
Created: 2025-05-09T00:37:38Z | Merged: 2025-05-13T19:43:11Z

## 🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-17111

## 📔 Objective

Update the login approving device types to match more closely what it is doing with updating the typings.

## 📸 Screenshots

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes



Diff:
diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
index 2dc1f2926b19..34a9d6c573e4 100644
--- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
+++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
@@ -6,6 +6,7 @@
 using Bit.Core.Entities;
 using Bit.Core.Enums;
 using Bit.Core.Repositories;
+using Bit.Core.Utilities;
 using Bit.Identity.Utilities;
 
 namespace Bit.Identity.IdentityServer;
@@ -24,7 +25,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
 
     private UserDecryptionOptions _options = new UserDecryptionOptions();
     private User? _user;
-    private Core.Auth.Entities.SsoConfig? _ssoConfig;
+    private SsoConfig? _ssoConfig;
     private Device? _device;
 
     public UserDecryptionOptionsBuilder(
@@ -45,7 +46,7 @@ public IUserDecryptionOptionsBuilder ForUser(User user)
         return this;
     }
 
-    public IUserDecryptionOptionsBuilder WithSso(Core.Auth.Entities.SsoConfig ssoConfig)
+    public IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig)
     {
         _ssoConfig = ssoConfig;
         return this;
@@ -119,8 +120,7 @@ private async Task BuildTrustedDeviceOptions()
             // 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
-                .Where(d => d.Identifier != _device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
-                .Any();
+                .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
new file mode 100644
index 000000000000..dd27a8755065
--- /dev/null
+++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs
@@ -0,0 +1,22 @@
+﻿using Bit.Core.Enums;
+
+namespace Bit.Identity.Utilities;
+
+public static class LoginApprovingClientTypes
+{
+    private static readonly IReadOnlyCollection<ClientType> _clientTypesThatCanApprove;
+
+    static LoginApprovingClientTypes()
+    {
+        var clientTypes = new List<ClientType>
+        {
+            ClientType.Desktop,
+            ClientType.Mobile,
+            ClientType.Web,
+            ClientType.Browser,
+        };
+        _clientTypesThatCanApprove = clientTypes.AsReadOnly();
+    }
+
+    public static IReadOnlyCollection<ClientType> TypesThatCanApprove => _clientTypesThatCanApprove;
+}
diff --git a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs b/src/Identity/Utilities/LoginApprovingDeviceTypes.cs
deleted file mode 100644
index b8b11a4d198b..000000000000
--- a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-﻿using Bit.Core.Enums;
-using Bit.Core.Utilities;
-
-namespace Bit.Identity.Utilities;
-
-public static class LoginApprovingDeviceTypes
-{
-    private static readonly IReadOnlyCollection<DeviceType> _deviceTypes;
-
-    static LoginApprovingDeviceTypes()
-    {
-        var deviceTypes = new List<DeviceType>();
-        deviceTypes.AddRange(DeviceTypes.DesktopTypes);
-        deviceTypes.AddRange(DeviceTypes.MobileTypes);
-        deviceTypes.AddRange(DeviceTypes.BrowserTypes);
-        _deviceTypes = deviceTypes.AsReadOnly();
-    }
-
-    public static IReadOnlyCollection<DeviceType> Types => _deviceTypes;
-}
diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs
index 89940275b01e..0a6346fe9eb2 100644
--- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs
+++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs
@@ -6,7 +6,6 @@
 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;
@@ -102,12 +101,39 @@ public async Task Build_WhenDeviceIsTrusted_ShouldReturnKeys(SsoConfig ssoConfig
         Assert.Equal(device.EncryptedUserKey, result.TrustedDeviceOption?.EncryptedUserKey);
     }
 
-    [Theory, BitAutoData]
-    public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
+    [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_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(
+        DeviceType deviceType,
+        SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
     {
         configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
         ssoConfig.Data = configurationData.Serialize();
-        approvingDevice.Type = LoginApprovingDeviceTypes.Types.First();
+        approvingDevice.Type = deviceType;
         _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice });
 
         var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync();
@@ -115,6 +141,24 @@ public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(Ss
         Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice);
     }
 
+    [Theory]
+    [BitAutoData(DeviceType.WindowsCLI)]
+    [BitAutoData(DeviceType.MacOsCLI)]
+    [BitAutoData(DeviceType.LinuxCLI)]
+    public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse(
+        DeviceType deviceType,
+        SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
+    {
+        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.False(result.TrustedDeviceOption?.HasLoginApprovingDevice);
+    }
+
     [Theory, BitAutoData]
     public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue(
         SsoConfig ssoConfig,


Reviews:
user: login: Patrick-Pimentel-Bitwarden
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T00:37:58Z
user: login: Patrick-Pimentel-Bitwarden
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:56:00Z
user: login: ike-kottlowski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T21:03:31Z

Review comments:
diff_hunk: @@ -24,7 +25,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
 
     private UserDecryptionOptions _options = new UserDecryptionOptions();
     private User? _user;
-    private Core.Auth.Entities.SsoConfig? _ssoConfig;
path: src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
user: login: Patrick-Pimentel-Bitwarden
body: Just cleaning up some other code
created_at: 2025-05-09T00:37:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,19 +101,64 @@ public async Task Build_WhenDeviceIsTrusted_ShouldReturnKeys(SsoConfig ssoConfig
         Assert.Equal(device.EncryptedUserKey, result.TrustedDeviceOption?.EncryptedUserKey);
     }
 
-    [Theory, BitAutoData]
-    public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
+    [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_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(
path: test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs
user: login: Patrick-Pimentel-Bitwarden
body: Hoping there's a better way to do this, this is very not flexible. If the list of clients ever grows then this will not necessarily get updated. Any ideas?
created_at: 2025-05-09T19:55:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-09T00:45:51Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [57aeebac\-4336\-4f62\-9447\-671310ad0524](https://ast.checkmarx.net/projects/fdc4f159-5309-4fab-8961-0f78c088bd72/scans?id=57aeebac-4336-4f62-9447-671310ad0524&branch=auth%2Fpm-17111%2Fadd-browser-to-list-of-approving-clients)
<!--fix issues section--><details><summary><b>Fixed Issues (8)</b></summary>
Great job! The following issues were fixed in this Pull Request

Severity|Issue|Source File / Package
---|---|---
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController\.cs: [22](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs#L22)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController\.cs: [81](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs#L81)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/AdminConsole/Controllers/SlackIntegrationController\.cs: [47](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/AdminConsole/Controllers/SlackIntegrationController.cs#L47)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/AdminConsole/Controllers/OrganizationIntegrationController\.cs: [23](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs#L23)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/AdminConsole/Controllers/OrganizationIntegrationController\.cs: [54](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs#L54)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/Controllers/PhishingDomainsController\.cs: [24](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/Controllers/PhishingDomainsController.cs#L24)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[CSRF](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/352/14408183907990125956)~~|/src/Api/Controllers/PhishingDomainsController\.cs: [12](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//src/Api/Controllers/PhishingDomainsController.cs#L12)
![MEDIUM](https://cdn.ast.checkmarx.net/integrations/severity/Medium.png)|~~[Use_Of_Hardcoded_Password](https://ast.checkmarx.net/results/fdc4f159-5309-4fab-8961-0f78c088bd72/57aeebac-4336-4f62-9447-671310ad0524/sast/description/259/5559909016429917497)~~|/util/Seeder/Factories/UserSeeder\.cs: [14](https://github.com/bitwarden/server/blob/auth/pm-17111/add-browser-to-list-of-approving-clients//util/Seeder/Factories/UserSeeder.cs#L14)
</details>

<!--fix issues section-->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-09T00:47:22Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/server/pull/5792?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 47.25%. Comparing base [(`c986cbb`)](https://app.codecov.io/gh/bitwarden/server/commit/c986cbb2086f296c7a0df9afec9aa7e8b981dd06?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`5bcffdb`)](https://app.codecov.io/gh/bitwarden/server/commit/5bcffdb067efe23e9090ce75f22d102911ea5225?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 107 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5792      +/-   ##
==========================================
+ Coverage   45.79%   47.25%   +1.45%     
==========================================
  Files        1609     1641      +32     
  Lines       73188    74723    +1535     
  Branches     6560     6737     +177     
==========================================
+ Hits        33517    35307    +1790     
+ Misses      38242    37948     -294     
- Partials     1429     1468      +39     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/server/pull/5792?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: Patrick-Pimentel-Bitwarden
created_at: 2025-05-09T19:55:07Z
body: @ike-kottlowski Would you have any idea how to not have to manually lay down each client type for the bit auto data and instead use the DeviceTypes.cs collection?
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T19:57:58Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bitwarden_server&pullRequest=5792) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bitwarden_server&pullRequest=5792&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bitwarden_server&pullRequest=5792&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bitwarden_server&pullRequest=5792&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bitwarden_server&pullRequest=5792&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bitwarden_server&pullRequest=5792&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bitwarden_server&pullRequest=5792)


author_association: NONE