Repository: bitwarden/server
Pull request #5840: [PM-18238] Add RequireTwoFactorPolicyRequirement
Author: login: r-tome
Created: 2025-05-20T09:42:50Z | Merged: 2025-05-29T06:40:30Z

## 🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-18238

## 📔 Objective

Add a new `RequireTwoFactorPolicyRequirement` to be used for checking if a user complies with the 2FA policy on various services (`UserService`, `RestoreOrganizationUserCommand`, `ConfirmOrganizationUserCommand`, and `AcceptOrgUserCommand`)

## ⏰ 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/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs
index f3426efddcc5..3770d867cfdf 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs
@@ -1,4 +1,6 @@
 ﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.Models.Business.Tokenables;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@@ -27,6 +29,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
     private readonly IUserRepository _userRepository;
     private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
     private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
+    private readonly IFeatureService _featureService;
+    private readonly IPolicyRequirementQuery _policyRequirementQuery;
 
     public AcceptOrgUserCommand(
         IDataProtectionProvider dataProtectionProvider,
@@ -37,9 +41,10 @@ public AcceptOrgUserCommand(
         IMailService mailService,
         IUserRepository userRepository,
         ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
-        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
+        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
+        IFeatureService featureService,
+        IPolicyRequirementQuery policyRequirementQuery)
     {
-
         // TODO: remove data protector when old token validation removed
         _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
         _globalSettings = globalSettings;
@@ -50,6 +55,8 @@ public AcceptOrgUserCommand(
         _userRepository = userRepository;
         _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
         _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
+        _featureService = featureService;
+        _policyRequirementQuery = policyRequirementQuery;
     }
 
     public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
@@ -196,15 +203,7 @@ public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser,
         }
 
         // Enforce Two Factor Authentication Policy of organization user is trying to join
-        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
-        {
-            var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
-                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
-            if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
-            {
-                throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
-            }
-        }
+        await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);
 
         orgUser.Status = OrganizationUserStatusType.Accepted;
         orgUser.UserId = user.Id;
@@ -224,4 +223,33 @@ public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser,
         return orgUser;
     }
 
+    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)
+    {
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
+            {
+                // If the user has two-step login enabled, we skip checking the 2FA policy
+                return;
+            }
+
+            var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
+            if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
+            {
+                throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
+            }
+
+            return;
+        }
+
+        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
+        {
+            var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
+                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
+            if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))
+            {
+                throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
+            }
+        }
+    }
 }
diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
index 9bfe8f791ee6..806cf5a533a5 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
@@ -1,5 +1,7 @@
 ﻿using Bit.Core.AdminConsole.Enums;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
 using Bit.Core.Billing.Enums;
@@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
     private readonly IPushRegistrationService _pushRegistrationService;
     private readonly IPolicyService _policyService;
     private readonly IDeviceRepository _deviceRepository;
+    private readonly IPolicyRequirementQuery _policyRequirementQuery;
+    private readonly IFeatureService _featureService;
 
     public ConfirmOrganizationUserCommand(
         IOrganizationRepository organizationRepository,
@@ -35,7 +39,9 @@ public ConfirmOrganizationUserCommand(
         IPushNotificationService pushNotificationService,
         IPushRegistrationService pushRegistrationService,
         IPolicyService policyService,
-        IDeviceRepository deviceRepository)
+        IDeviceRepository deviceRepository,
+        IPolicyRequirementQuery policyRequirementQuery,
+        IFeatureService featureService)
     {
         _organizationRepository = organizationRepository;
         _organizationUserRepository = organizationUserRepository;
@@ -47,6 +53,8 @@ public ConfirmOrganizationUserCommand(
         _pushRegistrationService = pushRegistrationService;
         _policyService = policyService;
         _deviceRepository = deviceRepository;
+        _policyRequirementQuery = policyRequirementQuery;
+        _featureService = featureService;
     }
 
     public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
@@ -118,8 +126,8 @@ public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid
                     }
                 }
 
-                var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
-                await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
+                var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
+                await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);
                 orgUser.Status = OrganizationUserStatusType.Confirmed;
                 orgUser.Key = keys[orgUser.Id];
                 orgUser.Email = null;
@@ -142,15 +150,10 @@ public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid
     }
 
     private async Task CheckPoliciesAsync(Guid organizationId, User user,
-        ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
+        ICollection<OrganizationUser> userOrgs, bool userTwoFactorEnabled)
     {
         // Enforce Two Factor Authentication Policy for this organization
-        var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
-            .Any(p => p.OrganizationId == organizationId);
-        if (orgRequiresTwoFactor && !twoFactorEnabled)
-        {
-            throw new BadRequestException("User does not have two-step login enabled.");
-        }
+        await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
 
         var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
         var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
@@ -168,6 +171,33 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
+    {
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            if (userTwoFactorEnabled)
+            {
+                // If the user has two-step login enabled, we skip checking the 2FA policy
+                return;
+            }
+
+            var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
+            if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
+            {
+                throw new BadRequestException("User does not have two-step login enabled.");
+            }
+
+            return;
+        }
+
+        var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
+            .Any(p => p.OrganizationId == organizationId);
+        if (orgRequiresTwoFactor && !userTwoFactorEnabled)
+        {
+            throw new BadRequestException("User does not have two-step login enabled.");
+        }
+    }
+
     private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
     {
         var devices = await GetUserDeviceIdsAsync(userId);
diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs
index 74165a5a7115..fe19cd1389ba 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs
@@ -1,5 +1,7 @@
 ﻿using Bit.Core.AdminConsole.Entities;
 using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
 using Bit.Core.Billing.Enums;
@@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand(
     ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
     IPolicyService policyService,
     IUserRepository userRepository,
-    IOrganizationService organizationService) : IRestoreOrganizationUserCommand
+    IOrganizationService organizationService,
+    IFeatureService featureService,
+    IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
 {
     public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
     {
@@ -270,12 +274,7 @@ private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, boo
         // Enforce 2FA Policy of organization user is trying to join
         if (!userHasTwoFactorEnabled)
         {
-            var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
-                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
-            if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
-            {
-                twoFactorCompliant = false;
-            }
+            twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId);
         }
 
         var user = await userRepository.GetByIdAsync(userId);
@@ -299,4 +298,17 @@ private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, boo
             throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
         }
     }
+
+    private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)
+    {
+        if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);
+            return requirement.IsTwoFactorRequiredForOrganization(organizationId);
+        }
+
+        var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
+            PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
+        return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId);
+    }
 }
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
new file mode 100644
index 000000000000..bbc997a83d85
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
@@ -0,0 +1,52 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.Enums;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Checks if two-factor authentication is required for the organization due to an active policy.
+    /// </summary>
+    /// <param name="organizationId">The ID of the organization to check.</param>
+    /// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
+    /// <remarks>
+    /// This should be used to check whether the member needs to have 2FA enabled before being
+    /// accepted, confirmed, or restored to the organization.
+    /// </remarks>
+    public bool IsTwoFactorRequiredForOrganization(Guid organizationId) =>
+        _policyDetails.Any(p => p.OrganizationId == organizationId);
+
+    /// <summary>
+    /// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required.
+    /// Users should be revoked from these organizations if they disable all 2FA methods.
+    /// </summary>
+    public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor =>
+        _policyDetails
+            .Where(p => p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed)
+            .Select(p => (p.OrganizationId, p.OrganizationUserId));
+}
+
+public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory<RequireTwoFactorPolicyRequirement>
+{
+    public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication;
+    protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
+
+    public override RequireTwoFactorPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
+    {
+        return new RequireTwoFactorPolicyRequirement(policyDetails);
+    }
+}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
index 1be0e61af72a..f98135b70da1 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
@@ -36,5 +36,6 @@ private static void AddPolicyRequirements(this IServiceCollection services)
         services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
         services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
         services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
+        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
     }
 }
diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
index 26ff4213286c..7640a82fcb56 100644
--- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
@@ -14,7 +14,6 @@
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.Enums;
 using Bit.Core.Auth.Repositories;
-using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
 using Bit.Core.Billing.Constants;
 using Bit.Core.Billing.Enums;
 using Bit.Core.Billing.Extensions;
@@ -45,7 +44,6 @@ public class OrganizationService : IOrganizationService
     private readonly IOrganizationRepository _organizationRepository;
     private readonly IOrganizationUserRepository _organizationUserRepository;
     private readonly ICollectionRepository _collectionRepository;
-    private readonly IUserRepository _userRepository;
     private readonly IGroupRepository _groupRepository;
     private readonly IMailService _mailService;
     private readonly IPushNotificationService _pushNotificationService;
@@ -69,7 +67,6 @@ public class OrganizationService : IOrganizationService
     private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
     private readonly IProviderRepository _providerRepository;
     private readonly IFeatureService _featureService;
-    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
     private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
     private readonly IPricingClient _pricingClient;
     private readonly IPolicyRequirementQuery _policyRequirementQuery;
@@ -79,7 +76,6 @@ public OrganizationService(
         IOrganizationRepository organizationRepository,
         IOrganizationUserRepository organizationUserRepository,
         ICollectionRepository collectionRepository,
-        IUserRepository userRepository,
         IGroupRepository groupRepository,
         IMailService mailService,
         IPushNotificationService pushNotificationService,
@@ -103,7 +99,6 @@ public OrganizationService(
         IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
         IProviderRepository providerRepository,
         IFeatureService featureService,
-        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
         IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
         IPricingClient pricingClient,
         IPolicyRequirementQuery policyRequirementQuery,
@@ -113,7 +108,6 @@ ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
         _organizationRepository = organizationRepository;
         _organizationUserRepository = organizationUserRepository;
         _collectionRepository = collectionRepository;
-        _userRepository = userRepository;
         _groupRepository = groupRepository;
         _mailService = mailService;
         _pushNotificationService = pushNotificationService;
@@ -137,7 +131,6 @@ ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
         _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
         _providerRepository = providerRepository;
         _featureService = featureService;
-        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
         _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
         _pricingClient = pricingClient;
         _policyRequirementQuery = policyRequirementQuery;
@@ -1722,72 +1715,6 @@ public async Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid o
         return result;
     }
 
-    private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
-    {
-        // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
-        // The user will be subject to the same checks when they try to accept the invite
-        if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
-        {
-            return;
-        }
-
-        var userId = orgUser.UserId.Value;
-
-        // Enforce Single Organization Policy of organization user is being restored to
-        var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
-        var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
-        var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId,
-            PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
-        var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
-
-        var singleOrgCompliant = true;
-        var belongsToOtherOrgCompliant = true;
-        var twoFactorCompliant = true;
-
-        if (hasOtherOrgs && singleOrgPolicyApplies)
-        {
-            singleOrgCompliant = false;
-        }
-
-        // Enforce Single Organization Policy of other organizations user is a member of
-        var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId,
-            PolicyType.SingleOrg);
-        if (anySingleOrgPolicies)
-        {
-            belongsToOtherOrgCompliant = false;
-        }
-
-        // Enforce Two Factor Authentication Policy of organization user is trying to join
-        if (!userHasTwoFactorEnabled)
-        {
-            var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
-                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
-            if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
-            {
-                twoFactorCompliant = false;
-            }
-        }
-
-        var user = await _userRepository.GetByIdAsync(userId);
-
-        if (!singleOrgCompliant && !twoFactorCompliant)
-        {
-            throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy");
-        }
-        else if (!singleOrgCompliant)
-        {
-            throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
-        }
-        else if (!belongsToOtherOrgCompliant)
-        {
-            throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations");
-        }
-        else if (!twoFactorCompliant)
-        {
-            throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
-        }
-    }
-
     public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
     {
         // Determine status to revert back to
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index 76520b408596..f0c97b8589b3 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -6,6 +6,8 @@
 using Bit.Core.AdminConsole.Models.Data;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Repositories;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.Enums;
@@ -81,6 +83,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
     private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
     private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
     private readonly IDistributedCache _distributedCache;
+    private readonly IPolicyRequirementQuery _policyRequirementQuery;
 
     public UserService(
         IUserRepository userRepository,
@@ -119,7 +122,8 @@ public UserService(
         IRemoveOrganizationUserCommand removeOrganizationUserCommand,
         IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
         ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
-        IDistributedCache distributedCache)
+        IDistributedCache distributedCache,
+        IPolicyRequirementQuery policyRequirementQuery)
         : base(
               store,
               optionsAccessor,
@@ -164,6 +168,7 @@ public UserService(
         _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
         _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
         _distributedCache = distributedCache;
+        _policyRequirementQuery = policyRequirementQuery;
     }
 
     public Guid? GetProperUserId(ClaimsPrincipal principal)
@@ -1394,9 +1399,40 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool set
 
     private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
     {
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
+            if (!requirement.OrganizationsRequiringTwoFactor.Any())
+            {
+                Logger.LogInformation("No organizations requiring two factor for user {userId}.", user.Id);
+                return;
+            }
+
+            var organizationIds = requirement.OrganizationsRequiringTwoFactor.Select(o => o.OrganizationId).ToList();
+            var organizations = await _organizationRepository.GetManyByIdsAsync(organizationIds);
+            var organizationLookup = organizations.ToDictionary(org => org.Id);
+
+            var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor
+                .Where(o => organizationLookup.ContainsKey(o.OrganizationId))
+                .Select(async o =>
+                {
+                    var organization = organizationLookup[o.OrganizationId];
+                    await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
+                        new RevokeOrganizationUsersRequest(
+                            o.OrganizationId,
+                            [new OrganizationUserUserDetails { Id = o.OrganizationUserId, OrganizationId = o.OrganizationId }],
+                            new SystemUser(EventSystemUser.TwoFactorDisabled)));
+                    await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
+                }).ToArray();
+
+            await Task.WhenAll(revokeOrgUserTasks);
+
+            return;
+        }
+
         var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
 
-        var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
+        var legacyRevokeOrgUserTasks = twoFactorPolicies.Select(async p =>
         {
             var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
             await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
@@ -1407,7 +1443,7 @@ await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationU
             await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
         }).ToArray();
 
-        await Task.WhenAll(removeOrgUserTasks);
+        await Task.WhenAll(legacyRevokeOrgUserTasks);
     }
 
     public override async Task<IdentityResult> ConfirmEmailAsync(User user, string token)
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs
index baf844acae91..540bac4d1c63 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs
@@ -1,5 +1,8 @@
 ﻿using Bit.Core.AdminConsole.Entities;
 using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.Models.Business.Tokenables;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@@ -29,7 +32,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
 public class AcceptOrgUserCommandTests
 {
     private readonly IUserService _userService = Substitute.For<IUserService>();
-    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
     private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();
     private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
 
@@ -166,9 +168,6 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB
         // Arrange
         SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
 
-        // User doesn't have 2FA enabled
-        _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
-
         // Organization they are trying to join requires 2FA
         var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };
         sutProvider.GetDependency<IPolicyService>()
@@ -185,6 +184,107 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB
             exception.Message);
     }
 
+    [Theory]
+    [BitAutoData]
+    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(
+        SutProvider<AcceptOrgUserCommand> sutProvider,
+        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
+    {
+        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        // Organization they are trying to join requires 2FA
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = orgUser.OrganizationId,
+                    OrganizationUserStatus = OrganizationUserStatusType.Invited,
+                    PolicyType = PolicyType.TwoFactorAuthentication,
+                }
+            ]));
+
+        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
+            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
+
+        Assert.Equal("You cannot join this organization until you enable two-step login on your user account.",
+            exception.Message);
+    }
+
+    [Theory]
+    [BitAutoData]
+    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds(
+        SutProvider<AcceptOrgUserCommand> sutProvider,
+        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
+    {
+        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        // User has 2FA enabled
+        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
+            .TwoFactorIsEnabledAsync(user)
+            .Returns(true);
+
+        // Organization they are trying to join requires 2FA
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = orgUser.OrganizationId,
+                    OrganizationUserStatus = OrganizationUserStatusType.Invited,
+                    PolicyType = PolicyType.TwoFactorAuthentication,
+                }
+            ]));
+
+        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
+
+        await sutProvider.GetDependency<IOrganizationUserRepository>()
+            .Received(1)
+            .ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
+    }
+
+    [Theory]
+    [BitAutoData]
+    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds(
+        SutProvider<AcceptOrgUserCommand> sutProvider,
+        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
+    {
+        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        // Organization they are trying to join doesn't require 2FA
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = Guid.NewGuid(),
+                    OrganizationUserStatus = OrganizationUserStatusType.Invited,
+                    PolicyType = PolicyType.TwoFactorAuthentication,
+                }
+            ]));
+
+        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
+
+        await sutProvider.GetDependency<IOrganizationUserRepository>()
+            .Received(1)
+            .ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
+    }
+
     [Theory]
     [BitAutoData(OrganizationUserType.Admin)]
     [BitAutoData(OrganizationUserType.Owner)]
@@ -647,9 +747,6 @@ private void SetupCommonAcceptOrgUserMocks(SutProvider<AcceptOrgUserCommand> sut
             .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
             .Returns(false);
 
-        // User doesn't have 2FA enabled
-        _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
-
         // Org does not require 2FA
         sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,
                 PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
index 06335f668dca..366d8cb2d626 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
@@ -1,6 +1,9 @@
 ﻿using Bit.Core.AdminConsole.Entities;
 using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
 using Bit.Core.Billing.Enums;
@@ -321,4 +324,122 @@ public async Task ConfirmUsersAsync_WithMultipleUsers_ReturnsExpectedMixedResult
         Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
         Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
     }
+
+    [Theory, BitAutoData]
+    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException(
+        Organization org, OrganizationUser confirmingUser,
+        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
+        SutProvider<ConfirmOrganizationUserCommand> sutProvider)
+    {
+        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
+        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
+        var userRepository = sutProvider.GetDependency<IUserRepository>();
+        var featureService = sutProvider.GetDependency<IFeatureService>();
+        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
+        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
+
+        org.PlanType = PlanType.EnterpriseAnnually;
+        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
+        orgUser.UserId = user.Id;
+        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
+        organizationRepository.GetByIdAsync(org.Id).Returns(org);
+        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
+        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
+        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = org.Id,
+                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
+
+        var exception = await Assert.ThrowsAsync<BadRequestException>(
+            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id));
+        Assert.Contains("User does not have two-step login enabled.", exception.Message);
+    }
+
+    [Theory, BitAutoData]
+    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds(
+        Organization org, OrganizationUser confirmingUser,
+        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
+        SutProvider<ConfirmOrganizationUserCommand> sutProvider)
+    {
+        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
+        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
+        var userRepository = sutProvider.GetDependency<IUserRepository>();
+        var featureService = sutProvider.GetDependency<IFeatureService>();
+        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
+        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
+
+        org.PlanType = PlanType.EnterpriseAnnually;
+        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
+        orgUser.UserId = user.Id;
+        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
+        organizationRepository.GetByIdAsync(org.Id).Returns(org);
+        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
+        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
+        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = Guid.NewGuid(),
+                    OrganizationUserStatus = OrganizationUserStatusType.Invited,
+                    PolicyType = PolicyType.TwoFactorAuthentication,
+                }
+            ]));
+        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
+
+        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
+
+        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
+        await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
+        await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
+    }
+
+    [Theory, BitAutoData]
+    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds(
+        Organization org, OrganizationUser confirmingUser,
+        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
+        SutProvider<ConfirmOrganizationUserCommand> sutProvider)
+    {
+        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
+        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
+        var userRepository = sutProvider.GetDependency<IUserRepository>();
+        var featureService = sutProvider.GetDependency<IFeatureService>();
+        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
+        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
+
+        org.PlanType = PlanType.EnterpriseAnnually;
+        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
+        orgUser.UserId = user.Id;
+        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
+        organizationRepository.GetByIdAsync(org.Id).Returns(org);
+        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
+        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
+        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = org.Id,
+                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
+
+        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
+
+        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
+        await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
+        await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
+    }
 }
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs
index d6880a3a1215..fbd711307c0b 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs
@@ -1,6 +1,9 @@
 ﻿using Bit.Core.AdminConsole.Entities;
 using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
 using Bit.Core.Billing.Enums;
@@ -208,6 +211,57 @@ await sutProvider.GetDependency<IPushNotificationService>()
             .PushSyncOrgKeysAsync(Arg.Any<Guid>());
     }
 
+    [Theory, BitAutoData]
+    public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
+        Organization organization,
+        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
+        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
+        SutProvider<RestoreOrganizationUserCommand> sutProvider)
+    {
+        organizationUser.Email = null;
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
+            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
+
+        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
+
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organizationUser.OrganizationId,
+                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+
+        var user = new User();
+        user.Email = "test@bitwarden.com";
+        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
+
+        var exception = await Assert.ThrowsAsync<BadRequestException>(
+            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
+
+        Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
+
+        await sutProvider.GetDependency<IOrganizationUserRepository>()
+            .DidNotReceiveWithAnyArgs()
+            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
+        await sutProvider.GetDependency<IEventService>()
+            .DidNotReceiveWithAnyArgs()
+            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
+        await sutProvider.GetDependency<IPushNotificationService>()
+            .DidNotReceiveWithAnyArgs()
+            .PushSyncOrgKeysAsync(Arg.Any<Guid>());
+    }
+
     [Theory, BitAutoData]
     public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
         Organization organization,
@@ -236,49 +290,47 @@ await sutProvider.GetDependency<IEventService>()
     }
 
     [Theory, BitAutoData]
-    public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
+    public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
         Organization organization,
         [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
         [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
-        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
         SutProvider<RestoreOrganizationUserCommand> sutProvider)
     {
         organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
-        secondOrganizationUser.UserId = organizationUser.UserId;
-        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
 
-        sutProvider.GetDependency<IOrganizationUserRepository>()
-            .GetManyByUserAsync(organizationUser.UserId.Value)
-            .Returns(new[] { organizationUser, secondOrganizationUser });
-        sutProvider.GetDependency<IPolicyService>()
-            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
-            .Returns(new[]
-            {
-                new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
-            });
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
 
-        var user = new User();
-        user.Email = "test@bitwarden.com";
-        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
+        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
 
-        var exception = await Assert.ThrowsAsync<BadRequestException>(
-            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
+        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
+            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organizationUser.OrganizationId,
+                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
 
-        Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
+        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
 
         await sutProvider.GetDependency<IOrganizationUserRepository>()
-            .DidNotReceiveWithAnyArgs()
-            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
+            .Received(1)
+            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
         await sutProvider.GetDependency<IEventService>()
-            .DidNotReceiveWithAnyArgs()
-            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
-        await sutProvider.GetDependency<IPushNotificationService>()
-            .DidNotReceiveWithAnyArgs()
-            .PushSyncOrgKeysAsync(Arg.Any<Guid>());
+            .Received(1)
+            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
     }
 
     [Theory, BitAutoData]
-    public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
+    public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
         Organization organization,
         [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
         [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
@@ -289,21 +341,24 @@ public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_
         secondOrganizationUser.UserId = organizationUser.UserId;
         RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
 
-        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
-            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
-            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
-
+        sutProvider.GetDependency<IOrganizationUserRepository>()
+            .GetManyByUserAsync(organizationUser.UserId.Value)
+            .Returns(new[] { organizationUser, secondOrganizationUser });
         sutProvider.GetDependency<IPolicyService>()
-            .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
-            .Returns(true);
+            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
+            .Returns(new[]
+            {
+                new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
+            });
 
-        var user = new User { Email = "test@bitwarden.com" };
+        var user = new User();
+        user.Email = "test@bitwarden.com";
         sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
 
         var exception = await Assert.ThrowsAsync<BadRequestException>(
             () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
 
-        Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
+        Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
 
         await sutProvider.GetDependency<IOrganizationUserRepository>()
             .DidNotReceiveWithAnyArgs()
@@ -364,20 +419,42 @@ await sutProvider.GetDependency<IPushNotificationService>()
     }
 
     [Theory, BitAutoData]
-    public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
+    public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
         Organization organization,
         [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
         [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
+        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
         SutProvider<RestoreOrganizationUserCommand> sutProvider)
     {
-        organizationUser.Email = null;
-
+        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
+        secondOrganizationUser.UserId = organizationUser.UserId;
         RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
 
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        sutProvider.GetDependency<IOrganizationUserRepository>()
+            .GetManyByUserAsync(organizationUser.UserId.Value)
+            .Returns(new[] { organizationUser, secondOrganizationUser });
         sutProvider.GetDependency<IPolicyService>()
-            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
-            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
-            ]);
+            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
+            .Returns(new[]
+            {
+                new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
+            });
+
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organizationUser.OrganizationId,
+                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
 
         var user = new User { Email = "test@bitwarden.com" };
         sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
@@ -385,7 +462,7 @@ public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigure
         var exception = await Assert.ThrowsAsync<BadRequestException>(
             () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
 
-        Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
+        Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
 
         await sutProvider.GetDependency<IOrganizationUserRepository>()
             .DidNotReceiveWithAnyArgs()
@@ -398,35 +475,6 @@ await sutProvider.GetDependency<IPushNotificationService>()
             .PushSyncOrgKeysAsync(Arg.Any<Guid>());
     }
 
-    [Theory, BitAutoData]
-    public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
-        Organization organization,
-        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
-        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
-        SutProvider<RestoreOrganizationUserCommand> sutProvider)
-    {
-        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
-        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
-
-        sutProvider.GetDependency<IPolicyService>()
-            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
-            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
-            ]);
-
-        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
-            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
-            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
-
-        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
-
-        await sutProvider.GetDependency<IOrganizationUserRepository>()
-            .Received(1)
-            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
-        await sutProvider.GetDependency<IEventService>()
-            .Received(1)
-            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
-    }
-
     [Theory, BitAutoData]
     public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(
         Organization organization,
@@ -672,6 +720,77 @@ await organizationUserRepository
             .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
     }
 
+    [Theory, BitAutoData]
+    public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization,
+        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
+        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
+        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
+        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
+        SutProvider<RestoreOrganizationUserCommand> sutProvider)
+    {
+        // Arrange
+        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+
+        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
+        var userRepository = sutProvider.GetDependency<IUserRepository>();
+        var policyService = sutProvider.GetDependency<IPolicyService>();
+        var userService = Substitute.For<IUserService>();
+
+        orgUser1.Email = orgUser2.Email = null;
+        orgUser3.UserId = null;
+        orgUser3.Key = null;
+        orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
+        organizationUserRepository
+            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
+            .Returns(new[] { orgUser1, orgUser2, orgUser3 });
+
+        userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
+
+        // Setup 2FA policy
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(Arg.Any<Guid>())
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organization.Id,
+                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+
+        // User1 has 2FA, User2 doesn't
+        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
+            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
+            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
+            {
+                (orgUser1.UserId!.Value, true),
+                (orgUser2.UserId!.Value, false)
+            });
+
+        // Act
+        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
+
+        // Assert
+        Assert.Equal(3, result.Count);
+        Assert.Empty(result[0].Item2); // First user should succeed
+        Assert.Contains("two-step login", result[1].Item2); // Second user should fail
+        Assert.Empty(result[2].Item2); // Third user should succeed
+        await organizationUserRepository
+            .Received(1)
+            .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
+        await organizationUserRepository
+            .DidNotReceive()
+            .RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());
+        await organizationUserRepository
+            .Received(1)
+            .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
+    }
+
     [Theory, BitAutoData]
     public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
         [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs
new file mode 100644
index 000000000000..c20ea494abe7
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs
@@ -0,0 +1,117 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Xunit;
+
+namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+[SutProviderCustomize]
+public class RequireTwoFactorPolicyRequirementFactoryTests
+{
+    [Theory]
+    [BitAutoData]
+    public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse(
+        Guid organizationId,
+        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
+    {
+        var actual = sutProvider.Sut.Create([]);
+
+        Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
+    }
+
+    [Theory]
+    [BitAutoData]
+    public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue(
+        Guid organizationId,
+        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
+    {
+        var actual = sutProvider.Sut.Create(
+        [
+            new PolicyDetails
+            {
+                OrganizationId = organizationId,
+                PolicyType = PolicyType.TwoFactorAuthentication,
+            }
+        ]);
+
+        Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId));
+    }
+
+    [Theory]
+    [BitAutoData]
+    public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse(
+        Guid organizationId,
+        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
+    {
+        var actual = sutProvider.Sut.Create(
+        [
+            new PolicyDetails
+            {
+                OrganizationId = Guid.NewGuid(),
+                PolicyType = PolicyType.TwoFactorAuthentication,
+            },
+        ]);
+
+        Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
+    }
+
+    [Theory, BitAutoData]
+    public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection(
+        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
+    {
+        var actual = sutProvider.Sut.Create([]);
+
+        Assert.Empty(actual.OrganizationsRequiringTwoFactor);
+    }
+
+    [Theory, BitAutoData]
+    public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships(
+        Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2,
+        Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4,
+        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
+    {
+        var policies = new[]
+        {
+            new PolicyDetails
+            {
+                OrganizationId = orgId1,
+                OrganizationUserId = orgUserId1,
+                PolicyType = PolicyType.TwoFactorAuthentication,
+                OrganizationUserStatus = OrganizationUserStatusType.Accepted
+            },
+            new PolicyDetails
+            {
+                OrganizationId = orgId2,
+                OrganizationUserId = orgUserId2,
+                PolicyType = PolicyType.TwoFactorAuthentication,
+                OrganizationUserStatus = OrganizationUserStatusType.Confirmed
+            },
+            new PolicyDetails
+            {
+                OrganizationId = orgId3,
+                OrganizationUserId = orgUserId3,
+                PolicyType = PolicyType.TwoFactorAuthentication,
+                OrganizationUserStatus = OrganizationUserStatusType.Invited
+            },
+            new PolicyDetails
+            {
+                OrganizationId = orgId4,
+                OrganizationUserId = orgUserId4,
+                PolicyType = PolicyType.TwoFactorAuthentication,
+                OrganizationUserStatus = OrganizationUserStatusType.Revoked
+            }
+        };
+
+        var actual = sutProvider.Sut.Create(policies);
+
+        var result = actual.OrganizationsRequiringTwoFactor.ToList();
+        Assert.Equal(2, result.Count);
+        Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1);
+        Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2);
+        Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3);
+        Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4);
+    }
+}
diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs
index ac7f6e401866..a5bfe35152fc 100644
--- a/test/Core.Test/Services/UserServiceTests.cs
+++ b/test/Core.Test/Services/UserServiceTests.cs
@@ -2,8 +2,11 @@
 using System.Text.Json;
 using Bit.Core.AdminConsole.Entities;
 using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
 using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
 using Bit.Core.AdminConsole.Repositories;
 using Bit.Core.AdminConsole.Services;
 using Bit.Core.Auth.Enums;
@@ -326,7 +329,8 @@ public async Task VerifySecretAsync_Works(
             sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
             sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
             sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
-            sutProvider.GetDependency<IDistributedCache>()
+            sutProvider.GetDependency<IDistributedCache>(),
+            sutProvider.GetDependency<IPolicyRequirementQuery>()
             );
 
         var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@@ -462,6 +466,78 @@ await sutProvider.GetDependency<IMailService>()
             .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
     }
 
+    [Theory, BitAutoData]
+    public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
+        SutProvider<UserService> sutProvider, User user,
+        Organization organization1, Guid organizationUserId1,
+        Organization organization2, Guid organizationUserId2)
+    {
+        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
+        {
+            [TwoFactorProviderType.Email] = new() { Enabled = true }
+        });
+        organization1.Enabled = organization2.Enabled = true;
+        organization1.UseSso = organization2.UseSso = true;
+
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organization1.Id,
+                    OrganizationUserId = organizationUserId1,
+                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                },
+                new PolicyDetails
+                {
+                    OrganizationId = organization2.Id,
+                    OrganizationUserId = organizationUserId2,
+                    OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+        sutProvider.GetDependency<IOrganizationRepository>()
+            .GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id)))
+            .Returns(new[] { organization1, organization2 });
+        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
+
+        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
+
+        await sutProvider.GetDependency<IUserRepository>()
+            .Received(1)
+            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
+        await sutProvider.GetDependency<IEventService>()
+            .Received(1)
+            .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
+
+        // Revoke the user from the first organization
+        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
+            .Received(1)
+            .RevokeNonCompliantOrganizationUsersAsync(
+                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
+                    r.OrganizationUsers.First().Id == organizationUserId1 &&
+                    r.OrganizationUsers.First().OrganizationId == organization1.Id));
+        await sutProvider.GetDependency<IMailService>()
+            .Received(1)
+            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);
+
+        // Remove the user from the second organization
+        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
+            .Received(1)
+            .RevokeNonCompliantOrganizationUsersAsync(
+                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&
+                    r.OrganizationUsers.First().Id == organizationUserId2 &&
+                    r.OrganizationUsers.First().OrganizationId == organization2.Id));
+        await sutProvider.GetDependency<IMailService>()
+            .Received(1)
+            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
+    }
+
     [Theory, BitAutoData]
     public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
         SutProvider<UserService> sutProvider, User user, Organization organization)
@@ -509,6 +585,53 @@ await sutProvider.GetDependency<IMailService>()
             .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
     }
 
+    [Theory, BitAutoData]
+    public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
+        SutProvider<UserService> sutProvider, User user, Organization organization)
+    {
+        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
+        {
+            [TwoFactorProviderType.Email] = new() { Enabled = true },
+            [TwoFactorProviderType.Remember] = new() { Enabled = true }
+        });
+        sutProvider.GetDependency<IFeatureService>()
+            .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+            .Returns(true);
+        sutProvider.GetDependency<IPolicyRequirementQuery>()
+            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
+            .Returns(new RequireTwoFactorPolicyRequirement(
+            [
+                new PolicyDetails
+                {
+                    OrganizationId = organization.Id,
+                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,
+                    PolicyType = PolicyType.TwoFactorAuthentication
+                }
+            ]));
+        sutProvider.GetDependency<IOrganizationRepository>()
+            .GetByIdAsync(organization.Id)
+            .Returns(organization);
+        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
+            .TwoFactorIsEnabledAsync(user)
+            .Returns(true);
+        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
+        {
+            [TwoFactorProviderType.Remember] = new() { Enabled = true }
+        }, JsonHelpers.LegacyEnumKeyResolver);
+
+        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
+
+        await sutProvider.GetDependency<IUserRepository>()
+            .Received(1)
+            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
+        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
+            .DidNotReceiveWithAnyArgs()
+            .RevokeNonCompliantOrganizationUsersAsync(default);
+        await sutProvider.GetDependency<IMailService>()
+            .DidNotReceiveWithAnyArgs()
+            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
+    }
+
     [Theory, BitAutoData]
     public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled(
         SutProvider<UserService> sutProvider, string email, string secret)
@@ -800,7 +923,8 @@ private IUserService RebuildSut(SutProvider<UserService> sutProvider)
             sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
             sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
             sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
-            sutProvider.GetDependency<IDistributedCache>()
+            sutProvider.GetDependency<IDistributedCache>(),
+            sutProvider.GetDependency<IPolicyRequirementQuery>()
             );
     }
 }


Reviews:
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:54:35Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:55:31Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Adds support for a new RequireTwoFactor policy requirement and updates the “accept” and “confirm” organization user commands to enforce it behind a feature flag.

- Introduce `RequireTwoFactorPolicyRequirement` and its factory for two-factor enforcement.
- Extend `AcceptOrgUserCommand` and `ConfirmOrganizationUserCommand` to use `IPolicyRequirementQuery` when the `PolicyRequirements` feature flag is enabled.
- Add unit tests covering both accept and confirm flows under the new two-factor policy.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                                           | Description                                                      |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs | New policy requirement and factory for two-factor authentication. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs        | Wire up feature flag + policy requirement query in confirm logic. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs                 | Wire up feature flag + policy requirement query in accept logic.  |
| test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs | Tests for the new policy requirement factory.                    |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs | Tests covering confirm behavior with two-factor policy.          |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs      | Tests covering accept behavior with two-factor policy.           |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs:31**
* The namespace is missing `AdminConsole` (should be `Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers`) to match the folder structure and other test files.
```
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
```
**test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs:444**
* Add tests for the scenario where the `PolicyRequirements` feature flag is disabled to ensure the fallback logic via `_policyService` still behaves correctly.
```
// end of existing tests
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:55:48Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces the RequireTwoFactorPolicyRequirement to enforce two‐factor authentication policies consistently across various user management commands. Key changes include updating tests and services (UserService, RestoreOrganizationUserCommand, ConfirmOrganizationUserCommand, and AcceptOrgUserCommand) to leverage the new policy requirement for two‐factor authentication, and refactoring dependency injections to support policy queries.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| test/Core.Test/Services/UserServiceTests.cs | Added dependency on IPolicyRequirementQuery and updated tests to verify two‐factor policy revocations. |
| test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs | Introduced and validated the policy requirement creation and behavior. |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs | Updated tests to assert behavior changes with policy requirements for 2FA. |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs | Adjusted tests to enforce policy checks during confirmation. |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs | Modified tests to validate error and success paths with policy requirements. |
| src/Core/Services/Implementations/UserService.cs | Integrated IPolicyRequirementQuery into two‐factor removal checks. |
| src/Core/AdminConsole/Services/Implementations/OrganizationService.cs | Removed unused two‐factor query dependency as policies are now enforced via policy requirements. |
| src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs | Added new policy requirement class and its factory for two‐factor authentication. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs | Updated restore logic to factor in the new two‐factor policy requirements. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs | Introduced enhanced policy validation for user confirmation. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs | Incorporated policy-based two‐factor authentication validation during acceptance of organization users. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs:206**
* [nitpick] Consider unifying the error message terminology to consistently refer to 'two-factor authentication' rather than 'two-step login', aligning with the rest of the codebase.
```
throw new BadRequestException("User does not have two-step login enabled.");
```
**src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs:261**
* [nitpick] Consider standardizing error messages across commands by replacing 'two-step login' with 'two-factor authentication' for consistency.
```
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:47:13Z
user: login: BTreston
body: Looks good, just some non-blocking thoughts that might be worth investigating
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T15:29:01Z
user: login: eliykat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T05:33:15Z
user: login: eliykat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T05:40:32Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new RequireTwoFactorPolicyRequirement along with the matching factory to enforce two‐factor authentication policies across multiple user management commands. Key changes include:
- Implementation of the new 2FA policy requirement and its corresponding factory.
- Updates to user management commands (UserService, Restore, Confirm, and Accept commands) to incorporate the 2FA policy checks based on a feature flag.
- Addition of extensive unit tests for validating the new 2FA policy behaviors in various user scenarios.

### Reviewed Changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| test/Core.Test/Services/UserServiceTests.cs | Updated tests to validate behavior when revoking 2FA providers using the new 2FA policy requirement. |
| test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs | Added unit tests for the new RequireTwoFactorPolicyRequirement factory. |
| test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/* | Enhanced tests across Restore, Confirm, and Accept commands to check for correct 2FA policy enforcement. |
| src/Core/Services/Implementations/UserService.cs | Injected and used IPolicyRequirementQuery for enforcing 2FA policies during provider removal. |
| src/Core/AdminConsole/OrganizationFeatures/Policies/* | Introduced the new policy requirement implementation and factory for two-factor authentication. |
| src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/* | Adjusted restore, confirm, and accept commands to conditionally enforce the new 2FA requirement when the feature flag is enabled. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs:129**
* [nitpick] Consider ensuring consistency in naming the two-factor enablement flag across methods. In some areas it is referenced as 'twoFactorEnabled' while here it is renamed to 'userTwoFactorEnabled'.
```
var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:57:57Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:35:39Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:42:06Z
user: login: eliykat
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-26T04:09:44Z
user: login: eliykat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-26T04:10:03Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T10:21:51Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T10:26:44Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T10:31:49Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T10:45:23Z
user: login: BTreston
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T16:02:09Z
user: login: eliykat
body: Other than @BTreston 's suggested change, looks good to me!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T01:46:12Z
user: login: r-tome
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T09:34:10Z
user: login: BTreston
body: Looks good 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T14:08:29Z
user: login: eliykat
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T00:09:03Z

Review comments:
diff_hunk: @@ -1782,72 +1775,6 @@ public async Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid o
         return result;
     }
 
-    private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
path: src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
user: login: r-tome
body: Unused method, leftover when lifting and shifting into command
created_at: 2025-05-21T14:54:35Z
author_association: CONTRIBUTOR
path: test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs
user: login: r-tome
body: vNext tests were redundant
created_at: 2025-05-21T14:55:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,6 +182,30 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
+    /// Validates the two-factor authentication policy for the organization user.
+    /// If the user has two-step login enabled, the policy is not enforced.
+    /// </summary>
+    /// <param name="user">The user to validate the policy for.</param>
+    /// <param name="organizationId">The ID of the organization to validate the policy for.</param>
+    /// <param name="userTwoFactorEnabled">Whether the user has two-step login enabled.</param>
+    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: Copilot
body: [nitpick] This validation method is largely duplicated in `AcceptOrgUserCommand`. Consider extracting shared two-factor validation logic into a common helper to reduce duplication.
created_at: 2025-05-21T14:55:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,6 +182,30 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: Copilot
body: The XML docs for `ValidateTwoFactorAuthenticationPolicyAsync` should include an `<exception>` tag to document that it throws `BadRequestException` if the policy requirement is not met.
created_at: 2025-05-21T14:55:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: BTreston
body: 💭 These seem like they would fit better in the factory method implementation below that initialize instance fields in the requirement class here. However, the current interface doesn't allow us to pass in additional data to generate the policy requirement apart from just `PolicyDetails` so I see why it is done this way. I wonder if its worth exploring expanding the factory interface to accommodate this kind of use case. The way it is now, the factory really just ends up being a wrapper for this class which seems counter intuitive imo.
created_at: 2025-05-22T15:24:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: eliykat
body: This is a good thought. I also don't love the `bool twoFactorEnabled` interface.

I can think of 2 alternatives:
1. you can inject whatever you want into the factory @BTreston, you're not limited to only the `PolicyDetails` that are passed in. (e.g. see the [SSO requirement](https://github.com/bitwarden/server/blob/198d96e1555d9c899db36728b37cbbcfc75f99f3/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirement.cs#L36-L40).) So you could inject the 2FA query and handle that inside the factory, rather than relying on the caller to tell you. However, the factory code is not currently async (no `Task`s) so that would require some refactoring to move to an async interface. I'm also not sure whether it's making the requirement responsible for too much, which leads to the second option.

2. limit how much this interface is doing: instead of `CanAcceptInvitation` make it more like `TwoFactorRequiredToAcceptInvitation`, then the caller decides whether to check 2FA based on the result. That wording is pretty bad. But I think this can be simplified further: `TwoFactorRequired(Guid organizationId) => _policyDetails.Any(p => p.OrganizationId == organizationId)` would work for all cases here. If you're confirming a user, restoring a user, or accepting an invite, what matters is the organization you're doing it for, not your status (which presumably the caller will check as part of the larger confirm/revoke flow etc). `TwoFactorPoliciesForActiveMemberships` is really the only method here that cares about the user status. The benefit of this approach is that the policy requirement only needs to communicate what the policy needs, and it doesn't know about the user's actual 2FA state. I think that's the better way to go.

This is the last thing on a Friday so please prove me wrong 😁 
created_at: 2025-05-23T05:31:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be confirmed in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be confirmed, false otherwise.</returns>
+    public bool CanBeConfirmed(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be restored in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be restored, false otherwise.</returns>
+    public bool CanBeRestored(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Revoked or
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+    /// <summary>
+    /// Gets the two-factor policies for active memberships.
+    /// </summary>
+    /// <returns>The two-factor policies for active memberships.</returns>
+    public IEnumerable<PolicyDetails> TwoFactorPoliciesForActiveMemberships =>
+        _policyDetails.Where(p => p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed);
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: eliykat
body: I think this could be more explicit. What does the policy _require_ - what is this interface used for? One of the goals of these requirements is that they describe the business logic.

For example:

```c#
    /// <summary>
    /// A list of organization IDs that require the user to have 2FA enabled. The user should be revoked from these organizations if they disable all 2FA methods.
    /// </summary>
    public IEnumerable<Guid> OrganizationsRequiringTwoFactor => 
```
created_at: 2025-05-23T05:40:32Z
author_association: MEMBER
diff_hunk: @@ -196,13 +203,20 @@ public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser,
         }
 
         // Enforce Two Factor Authentication Policy of organization user is trying to join
-        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs
user: login: Copilot
body: [nitpick] The inline conditional check for enforcing the two-factor policy could be unified by always delegating to the ValidateTwoFactorAuthenticationPolicyAsync method to reduce duplication and improve maintainability.
created_at: 2025-05-23T12:57:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be confirmed in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be confirmed, false otherwise.</returns>
+    public bool CanBeConfirmed(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be restored in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be restored, false otherwise.</returns>
+    public bool CanBeRestored(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Revoked or
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+    /// <summary>
+    /// Gets the two-factor policies for active memberships.
+    /// </summary>
+    /// <returns>The two-factor policies for active memberships.</returns>
+    public IEnumerable<PolicyDetails> TwoFactorPoliciesForActiveMemberships =>
+        _policyDetails.Where(p => p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed);
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: r-tome
body: I had to also return `OrganizationUserId` because that is used in `UserService` to revoke the user so I changed the return type to a Tuple to simplify it.
I also updated the xmldoc like you suggested.
created_at: 2025-05-23T13:35:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: r-tome
body: I decided to go with the second option. My first approach was to offload any policy logic from the calling places but that made it so that `RequireTwoFactorPolicyRequirement` was doing too much. 

Thank you both for the great thoughts!
created_at: 2025-05-23T13:42:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,51 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.Enums;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Checks if two-factor authentication is required for the organization due to an active policy.
+    /// </summary>
+    /// <param name="organizationId">The ID of the organization to check.</param>
+    /// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
+    /// <remarks>
+    /// This does not check the user's membership status.
+    /// </remarks>
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: eliykat
body: I'm not sure how useful this is. Does the caller need to know this? I also worry it'll make people check the member's status defensively even when it's not required.

If the concern is about outside callers using it properly, you could instead add more business context:

```c#
/// <remarks>
/// This should be used to check whether the member needs to have 2FA enabled before being
/// accepted, confirmed, or restored to the organization.
/// </remarks>
```

And/or if you're concerned about explaining yourself, you could move this to an internal comment for those who choose to peek under the hood:

```c#
// The member's status is implicitly checked by the caller and doesn't need to be checked here.
// This is basically a wrapper for whether the organization has the policy enabled.
_policyDetails.Any(p => p.OrganizationId == organizationId);
```
created_at: 2025-05-26T03:35:51Z
author_association: MEMBER
diff_hunk: @@ -196,13 +203,20 @@ public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser,
         }
 
         // Enforce Two Factor Authentication Policy of organization user is trying to join
-        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs
user: login: eliykat
body: I agree with the robot. You could move the old code into `ValidateTwoFactorAuthenticationPolicyAsync` as well, then do a feature flag check & early return within that method. I think regression risk on non-flagged code is minimal because it's just a cut & paste.
created_at: 2025-05-26T03:44:47Z
author_association: MEMBER
diff_hunk: @@ -142,14 +150,21 @@ public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid
     }
 
     private async Task CheckPoliciesAsync(Guid organizationId, User user,
-        ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
+        ICollection<OrganizationUser> userOrgs, bool userTwoFactorEnabled)
     {
         // Enforce Two Factor Authentication Policy for this organization
-        var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
-            .Any(p => p.OrganizationId == organizationId);
-        if (orgRequiresTwoFactor && !twoFactorEnabled)
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
         {
-            throw new BadRequestException("User does not have two-step login enabled.");
+            await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
+        }
+        else
+        {
+            var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
+                .Any(p => p.OrganizationId == organizationId);
+            if (orgRequiresTwoFactor && !userTwoFactorEnabled)
+            {
+                throw new BadRequestException("User does not have two-step login enabled.");
+            }
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: eliykat
body: Same here
created_at: 2025-05-26T03:47:09Z
author_association: MEMBER
diff_hunk: @@ -168,6 +183,32 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
+    /// Validates the two-factor authentication policy for the organization user.
+    /// If the policy applies to the organization, the user must have two-step login enabled.
+    /// </summary>
+    /// <param name="user">The user to validate the policy for.</param>
+    /// <param name="organizationId">The ID of the organization to validate the policy for.</param>
+    /// <param name="userTwoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <exception cref="BadRequestException">Thrown if the policy applies to the organization and 
+    /// the user does not have two-step login enabled.</exception>
+    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
+    {
+        if (userTwoFactorEnabled)
+        {
+            // If the user has two-step login enabled, we skip checking the 2FA policy
+            return;
+        }
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: eliykat
body: I think it would be better to be consistent: do we check the policy first, and then 2FA if required, or vice versa?

I assume you check this first here because you've already made the call to the 2FA query, but there's nothing stopping you from moving the 2FA query call down into these methods. It's called in the top method but not used anywhere else.
created_at: 2025-05-26T03:51:01Z
author_association: MEMBER
diff_hunk: @@ -168,6 +183,32 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
+    /// Validates the two-factor authentication policy for the organization user.
+    /// If the policy applies to the organization, the user must have two-step login enabled.
+    /// </summary>
+    /// <param name="user">The user to validate the policy for.</param>
+    /// <param name="organizationId">The ID of the organization to validate the policy for.</param>
+    /// <param name="userTwoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <exception cref="BadRequestException">Thrown if the policy applies to the organization and 
+    /// the user does not have two-step login enabled.</exception>
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: eliykat
body: I'm not sure whether this level of xmldoc is required for a fairly straightforward private method. You can leave it in if you think it's worthwhile though.

That said, the information about the policy:
> If the policy applies to the organization, the user must have two-step login enabled.

Is a good example of what the policy requirement can be used to document. It provides a single, central location to explain how the policy should be enforced (even though the actual check & throw is done elsewhere).
created_at: 2025-05-26T03:52:53Z
author_association: MEMBER
diff_hunk: @@ -1394,20 +1399,40 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool set
 
     private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
path: src/Core/Services/Implementations/UserService.cs
user: login: eliykat
body: Quite a bit of nesting here. Using an early return rather than `else` should clear it up a little.
created_at: 2025-05-26T03:55:18Z
author_association: MEMBER
diff_hunk: @@ -167,7 +169,7 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB
         SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
 
         // User doesn't have 2FA enabled
-        _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
path: test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs
user: login: eliykat
body: Is it possible to delete this class property altogether? I can't see that it does anything because it's not actually injected into the sut.
created_at: 2025-05-26T04:06:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Enums;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Determines if the user can accept an invitation to an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can accept the invitation, false otherwise.</returns>
+    public bool CanAcceptInvitation(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be confirmed in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be confirmed, false otherwise.</returns>
+    public bool CanBeConfirmed(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+
+    /// <summary>
+    /// Determines if the user can be restored in an organization.
+    /// </summary>
+    /// <param name="twoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <param name="organizationId">The ID of the organization.</param>
+    /// <returns>True if the user can be restored, false otherwise.</returns>
+    public bool CanBeRestored(bool twoFactorEnabled, Guid organizationId) =>
+        twoFactorEnabled ||
+        !_policyDetails.Any(p => p.OrganizationId == organizationId &&
+            (p.OrganizationUserStatus is
+                OrganizationUserStatusType.Revoked or
+                OrganizationUserStatusType.Invited or
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed));
+
+    /// <summary>
+    /// Gets the two-factor policies for active memberships.
+    /// </summary>
+    /// <returns>The two-factor policies for active memberships.</returns>
+    public IEnumerable<PolicyDetails> TwoFactorPoliciesForActiveMemberships =>
+        _policyDetails.Where(p => p.OrganizationUserStatus is
+                OrganizationUserStatusType.Accepted or
+                OrganizationUserStatusType.Confirmed);
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: eliykat
body: lgtm!
created_at: 2025-05-26T04:10:02Z
author_association: MEMBER
diff_hunk: @@ -168,6 +183,32 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
+    /// Validates the two-factor authentication policy for the organization user.
+    /// If the policy applies to the organization, the user must have two-step login enabled.
+    /// </summary>
+    /// <param name="user">The user to validate the policy for.</param>
+    /// <param name="organizationId">The ID of the organization to validate the policy for.</param>
+    /// <param name="userTwoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <exception cref="BadRequestException">Thrown if the policy applies to the organization and 
+    /// the user does not have two-step login enabled.</exception>
+    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
+    {
+        if (userTwoFactorEnabled)
+        {
+            // If the user has two-step login enabled, we skip checking the 2FA policy
+            return;
+        }
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: r-tome
body: I agree its better to be consistent, therefore I also changed `AcceptOrgUserCommand` to check 2FA first.

> there's nothing stopping you from moving the 2FA query call down into these methods

The 2FA check is done in bulk that's why I kept it at the top.
created_at: 2025-05-26T10:21:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,6 +183,32 @@ private async Task CheckPoliciesAsync(Guid organizationId, User user,
         }
     }
 
+    /// <summary>
+    /// Validates the two-factor authentication policy for the organization user.
+    /// If the policy applies to the organization, the user must have two-step login enabled.
+    /// </summary>
+    /// <param name="user">The user to validate the policy for.</param>
+    /// <param name="organizationId">The ID of the organization to validate the policy for.</param>
+    /// <param name="userTwoFactorEnabled">Whether the user has two-step login enabled.</param>
+    /// <exception cref="BadRequestException">Thrown if the policy applies to the organization and 
+    /// the user does not have two-step login enabled.</exception>
path: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
user: login: r-tome
body: I think I added xmldoc to this because I tried to move it to a public static method. I removed it altogether because the method is straightforward.
created_at: 2025-05-26T10:26:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,51 @@
+﻿using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.Enums;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+/// <summary>
+/// Policy requirements for the Require Two-Factor Authentication policy.
+/// </summary>
+public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
+{
+    private readonly IEnumerable<PolicyDetails> _policyDetails;
+
+    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
+    {
+        _policyDetails = policyDetails;
+    }
+
+    /// <summary>
+    /// Checks if two-factor authentication is required for the organization due to an active policy.
+    /// </summary>
+    /// <param name="organizationId">The ID of the organization to check.</param>
+    /// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
+    /// <remarks>
+    /// This does not check the user's membership status.
+    /// </remarks>
path: src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs
user: login: r-tome
body: I went with your first suggestion. This property is about if the policy applies to the organization, their user's membership status doesn't matter at this point.
created_at: 2025-05-26T10:31:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,7 +169,7 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB
         SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
 
         // User doesn't have 2FA enabled
-        _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
path: test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs
user: login: r-tome
body: It is used to check the user's 2FA. I removed redundant setups as this is already done in `SetupCommonAcceptOrgUserMocks`
created_at: 2025-05-26T10:45:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1394,9 +1399,29 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool set
 
     private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
     {
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
+
+            var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor.Select(async o =>
+            {
+                var organization = await _organizationRepository.GetByIdAsync(o.OrganizationId);
path: src/Core/Services/Implementations/UserService.cs
user: login: BTreston
body: I think we can use the bulk method for this instead and store in a dict keyed by `organizationId`
created_at: 2025-05-27T15:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1394,9 +1399,29 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool set
 
     private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
     {
+        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
+        {
+            var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
+
+            var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor.Select(async o =>
+            {
+                var organization = await _organizationRepository.GetByIdAsync(o.OrganizationId);
path: src/Core/Services/Implementations/UserService.cs
user: login: r-tome
body: Great idea!
created_at: 2025-05-28T09:34:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-20T09:52:27Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [2d68b0dd\-1875\-4c4f\-8f9e\-94ad78390e87](https://ast.checkmarx.net/projects/fdc4f159-5309-4fab-8961-0f78c088bd72/scans?id=2d68b0dd-1875-4c4f-8f9e-94ad78390e87&branch=ac%2Fpm-18238%2Fadd-requiretwofactorpolicyrequirement)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-20T16:09:05Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/server/pull/5840?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
Attention: Patch coverage is `95.23810%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 47.59%. Comparing base [(`c7b0c30`)](https://app.codecov.io/gh/bitwarden/server/commit/c7b0c30370ad320e9b974912a79b48473e4bd599?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`0e208c3`)](https://app.codecov.io/gh/bitwarden/server/commit/0e208c3243916513c2dd36cd2abded275536a42a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/bitwarden/server/pull/5840?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) | Patch % | Lines |
|---|---|---|
| [src/Core/Services/Implementations/UserService.cs](https://app.codecov.io/gh/bitwarden/server/pull/5840?src=pr&el=tree&filepath=src%2FCore%2FServices%2FImplementations%2FUserService.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-c3JjL0NvcmUvU2VydmljZXMvSW1wbGVtZW50YXRpb25zL1VzZXJTZXJ2aWNlLmNz) | 84.00% | [3 Missing and 1 partial :warning: ](https://app.codecov.io/gh/bitwarden/server/pull/5840?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |
| [...yRequirements/RequireTwoFactorPolicyRequirement.cs](https://app.codecov.io/gh/bitwarden/server/pull/5840?src=pr&el=tree&filepath=src%2FCore%2FAdminConsole%2FOrganizationFeatures%2FPolicies%2FPolicyRequirements%2FRequireTwoFactorPolicyRequirement.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-c3JjL0NvcmUvQWRtaW5Db25zb2xlL09yZ2FuaXphdGlvbkZlYXR1cmVzL1BvbGljaWVzL1BvbGljeVJlcXVpcmVtZW50cy9SZXF1aXJlVHdvRmFjdG9yUG9saWN5UmVxdWlyZW1lbnQuY3M=) | 93.33% | [1 Missing :warning: ](https://app.codecov.io/gh/bitwarden/server/pull/5840?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |

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


```diff
@@            Coverage Diff             @@
##             main    #5840      +/-   ##
==========================================
- Coverage   50.82%   47.59%   -3.23%     
==========================================
  Files        1662     1663       +1     
  Lines       75231    75260      +29     
  Branches     6762     6764       +2     
==========================================
- Hits        38235    35819    -2416     
- Misses      35472    37976    +2504     
+ Partials     1524     1465      -59     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/server/pull/5840?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: sonarqubecloud[bot]
created_at: 2025-05-29T06:23:48Z
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=5840) **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=5840&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=5840&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=5840&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=5840&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=5840&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bitwarden_server&pullRequest=5840)


author_association: NONE