1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

Merge branch 'refs/heads/main' into km/pm-10600

This commit is contained in:
Maciej Zieniuk 2024-10-28 15:55:46 +00:00
commit 89b0848002
No known key found for this signature in database
GPG Key ID: 9CACE59F1272ACD9
40 changed files with 1531 additions and 614 deletions

View File

@ -135,13 +135,61 @@ jobs:
git config --local user.email "actions@github.com" git config --local user.email "actions@github.com"
git config --local user.name "Github Actions" git config --local user.name "Github Actions"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Commit files - name: Commit files
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes - name: Push changes
run: git push
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create version PR
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: | run: |
git pull -pt PR_URL=$(gh pr create --title "$TITLE" \
git push --base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cherry_pick: cherry_pick:

View File

@ -379,42 +379,23 @@ public class ProviderBillingService(
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>(); var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
var teamsProviderPlan = foreach (var providerPlan in providerPlans)
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
{ {
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id); var plan = StaticStore.GetPlan(providerPlan.PlanType);
if (!providerPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
throw new BillingException(); throw new BillingException();
} }
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
subscriptionItemOptionsList.Add(new SubscriptionItemOptions subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{ {
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId, Price = plan.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = teamsProviderPlan.SeatMinimum Quantity = providerPlan.SeatMinimum
}); });
var enterpriseProviderPlan =
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Enterprise plan", provider.Id);
throw new BillingException();
} }
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = enterpriseProviderPlan.SeatMinimum
});
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions AutomaticTax = new SubscriptionAutomaticTaxOptions

View File

@ -53,6 +53,8 @@ public class OrganizationUsersController : Controller
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -73,7 +75,9 @@ public class OrganizationUsersController : Controller
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand) IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -94,29 +98,34 @@ public class OrganizationUsersController : Controller
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<OrganizationUserDetailsResponseModel> Get(string id, bool includeGroups = false) public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
{ {
var organizationUser = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(new Guid(id)); var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId)) if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2); var managedByOrganization = await GetManagedByOrganizationStatusAsync(
organizationUser.OrganizationId,
[organizationUser.Id]);
var response = new OrganizationUserDetailsResponseModel(organizationUser, managedByOrganization[organizationUser.Id], collections);
if (includeGroups) if (includeGroups)
{ {
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id); response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
} }
return response; return response;
} }
[HttpGet("mini-details")] [HttpGet("mini-details")]
[RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)]
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId) public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
{ {
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
@ -150,11 +159,13 @@ public class OrganizationUsersController : Controller
} }
); );
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id));
var responses = organizationUsers var responses = organizationUsers
.Select(o => .Select(o =>
{ {
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled); var managedByOrganization = organizationUsersManagementStatus[o.Id];
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization);
return orgUser; return orgUser;
}); });
@ -682,4 +693,15 @@ public class OrganizationUsersController : Controller
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r => return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
} }
private async Task<IDictionary<Guid, bool>> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds);
return usersOrganizationManagementStatus;
}
} }

View File

@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
{ {
public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser, public OrganizationUserDetailsResponseModel(
OrganizationUser organizationUser,
bool managedByOrganization,
IEnumerable<CollectionAccessSelection> collections) IEnumerable<CollectionAccessSelection> collections)
: base(organizationUser, "organizationUserDetails") : base(organizationUser, "organizationUserDetails")
{ {
ManagedByOrganization = managedByOrganization;
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
} }
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
bool managedByOrganization,
IEnumerable<CollectionAccessSelection> collections) IEnumerable<CollectionAccessSelection> collections)
: base(organizationUser, "organizationUserDetails") : base(organizationUser, "organizationUserDetails")
{ {
ManagedByOrganization = managedByOrganization;
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
} }
public bool ManagedByOrganization { get; set; }
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; } public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
{ {
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
bool twoFactorEnabled, string obj = "organizationUserUserDetails") bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails")
: base(organizationUser, obj) : base(organizationUser, obj)
{ {
if (organizationUser == null) if (organizationUser == null)
@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
Groups = organizationUser.Groups; Groups = organizationUser.Groups;
// Prevent reset password when using key connector. // Prevent reset password when using key connector.
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector; ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
ManagedByOrganization = managedByOrganization;
} }
public string Name { get; set; } public string Name { get; set; }
@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
public string AvatarColor { get; set; } public string AvatarColor { get; set; }
public bool TwoFactorEnabled { get; set; } public bool TwoFactorEnabled { get; set; }
public bool SsoBound { get; set; } public bool SsoBound { get; set; }
/// <summary>
/// Indicates if the organization manages the user. If a user is "managed" by an organization,
/// the organization has greater control over their account, and some user actions are restricted.
/// </summary>
public bool ManagedByOrganization { get; set; }
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; } public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; } public IEnumerable<Guid> Groups { get; set; }
} }

View File

@ -71,14 +71,13 @@ public class MembersController : Controller
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Get(Guid id) public async Task<IActionResult> Get(Guid id)
{ {
var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
var orgUser = userDetails?.Item1;
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId) if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
userDetails.Item2); collections);
return new JsonResult(response); return new JsonResult(response);
} }

View File

@ -0,0 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface IOrganizationHasVerifiedDomainsQuery
{
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
}

View File

@ -0,0 +1,10 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
public class OrganizationHasVerifiedDomainsQuery(IOrganizationDomainRepository domainRepository) : IOrganizationHasVerifiedDomainsQuery
{
public async Task<bool> HasVerifiedDomainsAsync(Guid orgId) =>
(await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null);
}

View File

@ -1,4 +1,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
private readonly IDnsResolverService _dnsResolverService; private readonly IDnsResolverService _dnsResolverService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IPolicyService _policyService;
private readonly IFeatureService _featureService;
private readonly IOrganizationService _organizationService;
private readonly ILogger<VerifyOrganizationDomainCommand> _logger; private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
public VerifyOrganizationDomainCommand( public VerifyOrganizationDomainCommand(
@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
IDnsResolverService dnsResolverService, IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IPolicyService policyService,
IFeatureService featureService,
IOrganizationService organizationService,
ILogger<VerifyOrganizationDomainCommand> logger) ILogger<VerifyOrganizationDomainCommand> logger)
{ {
_organizationDomainRepository = organizationDomainRepository; _organizationDomainRepository = organizationDomainRepository;
_dnsResolverService = dnsResolverService; _dnsResolverService = dnsResolverService;
_eventService = eventService; _eventService = eventService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_policyService = policyService;
_featureService = featureService;
_organizationService = organizationService;
_logger = logger; _logger = logger;
} }
@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
{ {
domain.SetVerifiedDate(); domain.SetVerifiedDate();
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
} }
} }
catch (Exception e) catch (Exception e)
@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
return domain; return domain;
} }
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
await _policyService.SaveAsync(
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null);
}
}
} }

View File

@ -1,7 +1,6 @@
#nullable enable #nullable enable
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
@ -10,12 +9,10 @@ public class OrganizationUserUserDetailsAuthorizationHandler
: AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope> : AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext, IFeatureService featureService) public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService;
} }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
@ -37,29 +34,6 @@ public class OrganizationUserUserDetailsAuthorizationHandler
} }
private async Task<bool> CanReadAllAsync(Guid organizationId) private async Task<bool> CanReadAllAsync(Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi))
{
return await CanReadAllAsync_vNext(organizationId);
}
return await CanReadAllAsync_vCurrent(organizationId);
}
private async Task<bool> CanReadAllAsync_vCurrent(Guid organizationId)
{
// All users of an organization can read all other users of that organization for collection access management
var org = _currentContext.GetOrganization(organizationId);
if (org is not null)
{
return true;
}
// Allow provider users to read all organization users if they are a provider for the target organization
return await _currentContext.ProviderUserForOrgAsync(organizationId);
}
private async Task<bool> CanReadAllAsync_vNext(Guid organizationId)
{ {
// Admins can access this for general user management // Admins can access this for general user management
var organization = _currentContext.GetOrganization(organizationId); var organization = _currentContext.GetOrganization(organizationId);

View File

@ -22,8 +22,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId); Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id); Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id); Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>> Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id);
GetDetailsByIdWithCollectionsAsync(Guid id);
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId, Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
OrganizationUserStatusType? status = null); OrganizationUserStatusType? status = null);

View File

@ -4,8 +4,4 @@ public interface IOrganizationDomainService
{ {
Task ValidateOrganizationsDomainAsync(); Task ValidateOrganizationsDomainAsync();
Task OrganizationDomainMaintenanceAsync(); Task OrganizationDomainMaintenanceAsync();
/// <summary>
/// Indicates if the organization has any verified domains.
/// </summary>
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
} }

View File

@ -106,12 +106,6 @@ public class OrganizationDomainService : IOrganizationDomainService
} }
} }
public async Task<bool> HasVerifiedDomainsAsync(Guid orgId)
{
var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId);
return orgDomains.Any(od => od.VerifiedDate != null);
}
private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId) private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)
{ {
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
@ -32,6 +33,7 @@ public class PolicyService : IPolicyService
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ISavePolicyCommand _savePolicyCommand; private readonly ISavePolicyCommand _savePolicyCommand;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
public PolicyService( public PolicyService(
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
@ -45,7 +47,8 @@ public class PolicyService : IPolicyService
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService, IFeatureService featureService,
ISavePolicyCommand savePolicyCommand, ISavePolicyCommand savePolicyCommand,
IRemoveOrganizationUserCommand removeOrganizationUserCommand) IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
{ {
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_eventService = eventService; _eventService = eventService;
@ -59,6 +62,7 @@ public class PolicyService : IPolicyService
_featureService = featureService; _featureService = featureService;
_savePolicyCommand = savePolicyCommand; _savePolicyCommand = savePolicyCommand;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
} }
public async Task SaveAsync(Policy policy, Guid? savingUserId) public async Task SaveAsync(Policy policy, Guid? savingUserId)
@ -239,6 +243,7 @@ public class PolicyService : IPolicyService
case PolicyType.SingleOrg: case PolicyType.SingleOrg:
if (!policy.Enabled) if (!policy.Enabled)
{ {
await HasVerifiedDomainsAsync(org);
await RequiredBySsoAsync(org); await RequiredBySsoAsync(org);
await RequiredByVaultTimeoutAsync(org); await RequiredByVaultTimeoutAsync(org);
await RequiredByKeyConnectorAsync(org); await RequiredByKeyConnectorAsync(org);
@ -279,6 +284,15 @@ public class PolicyService : IPolicyService
} }
} }
private async Task HasVerifiedDomainsAsync(Organization org)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
{
throw new BadRequestException("Organization has verified domains.");
}
}
private async Task SetPolicyConfiguration(Policy policy) private async Task SetPolicyConfiguration(Policy policy)
{ {
await _policyRepository.UpsertAsync(policy); await _policyRepository.UpsertAsync(policy);

View File

@ -87,7 +87,9 @@ public record EnterprisePlan : Plan
AdditionalStoragePricePerGb = 4; AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually"; StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2023-enterprise-org-seat-annually"; StripeSeatPlanId = "2023-enterprise-org-seat-annually";
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
SeatPrice = 72; SeatPrice = 72;
ProviderPortalSeatPrice = 72;
} }
else else
{ {

View File

@ -141,7 +141,6 @@ public static class FeatureFlagKeys
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string StorageReseedRefactor = "storage-reseed-refactor";
public const string TrialPayment = "PM-8163-trial-payment"; public const string TrialPayment = "PM-8163-trial-payment";
public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api";
public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string AccessIntelligence = "pm-13227-access-intelligence";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
@ -149,6 +148,7 @@ public static class FeatureFlagKeys
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions"; public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split"; public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -21,8 +21,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" /> <PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.24" /> <PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.30" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.34" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" /> <PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
@ -36,7 +36,7 @@
<PackageReference Include="Handlebars.Net" Version="2.1.6" /> <PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.44.0" /> <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" /> <PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
@ -59,7 +59,7 @@
<PackageReference Include="Otp.NET" Version="1.4.0" /> <PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" /> <PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" />
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.5.2" /> <PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>(); services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>(); services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>(); services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();
} }
private static void AddOrganizationAuthCommands(this IServiceCollection services) private static void AddOrganizationAuthCommands(this IServiceCollection services)

View File

@ -1,4 +1,5 @@
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Identity.IdentityServer.RequestValidators;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer;

View File

@ -1,15 +1,10 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -17,32 +12,26 @@ using Bit.Core.Enums;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Api.Response; using Bit.Core.Models.Api.Response;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer.RequestValidators;
public abstract class BaseRequestValidator<T> where T : class public abstract class BaseRequestValidator<T> where T : class
{ {
private UserManager<User> _userManager; private UserManager<User> _userManager;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IDeviceValidator _deviceValidator; private readonly IDeviceValidator _deviceValidator;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
protected ICurrentContext CurrentContext { get; } protected ICurrentContext CurrentContext { get; }
protected IPolicyService PolicyService { get; } protected IPolicyService PolicyService { get; }
@ -56,18 +45,14 @@ public abstract class BaseRequestValidator<T> where T : class
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger logger, ILogger logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
@ -76,18 +61,14 @@ public abstract class BaseRequestValidator<T> where T : class
_userService = userService; _userService = userService;
_eventService = eventService; _eventService = eventService;
_deviceValidator = deviceValidator; _deviceValidator = deviceValidator;
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
_duoWebV4SDKService = duoWebV4SDKService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
_mailService = mailService; _mailService = mailService;
_logger = logger; _logger = logger;
CurrentContext = currentContext; CurrentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
PolicyService = policyService; PolicyService = policyService;
_userRepository = userRepository; _userRepository = userRepository;
_tokenDataFactory = tokenDataFactory;
FeatureService = featureService; FeatureService = featureService;
SsoConfigRepository = ssoConfigRepository; SsoConfigRepository = ssoConfigRepository;
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
@ -104,12 +85,6 @@ public abstract class BaseRequestValidator<T> where T : class
request.UserName, validatorContext.CaptchaResponse.Score); request.UserName, validatorContext.CaptchaResponse.Score);
} }
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider);
var valid = await ValidateContextAsync(context, validatorContext); var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User; var user = validatorContext.User;
if (!valid) if (!valid)
@ -123,17 +98,37 @@ public abstract class BaseRequestValidator<T> where T : class
return; return;
} }
var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider);
if (isTwoFactorRequired) if (isTwoFactorRequired)
{ {
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) // 2FA required and not provided response
if (!validTwoFactorRequest ||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
{ {
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); var resultDict = await _twoFactorAuthenticationValidator
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
if (resultDict == null)
{
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
return; return;
} }
var verified = await VerifyTwoFactor(user, twoFactorOrganization, // Include Master Password Policy in 2FA response
twoFactorProviderType, twoFactorToken); resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
SetTwoFactorResult(context, resultDict);
return;
}
var verified = await _twoFactorAuthenticationValidator
.VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
// 2FA required but request not valid or remember token expired response
if (!verified || isBot) if (!verified || isBot)
{ {
if (twoFactorProviderType != TwoFactorProviderType.Remember) if (twoFactorProviderType != TwoFactorProviderType.Remember)
@ -143,16 +138,20 @@ public abstract class BaseRequestValidator<T> where T : class
} }
else if (twoFactorProviderType == TwoFactorProviderType.Remember) else if (twoFactorProviderType == TwoFactorProviderType.Remember)
{ {
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); var resultDict = await _twoFactorAuthenticationValidator
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
// Include Master Password Policy in 2FA response
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
SetTwoFactorResult(context, resultDict);
} }
return; return;
} }
} }
else else
{ {
twoFactorRequest = false; validTwoFactorRequest = false;
twoFactorRemember = false; twoFactorRemember = false;
twoFactorToken = null;
} }
// Force legacy users to the web for migration // Force legacy users to the web for migration
@ -165,7 +164,6 @@ public abstract class BaseRequestValidator<T> where T : class
} }
} }
// Returns true if can finish validation process
if (await IsValidAuthTypeAsync(user, request.GrantType)) if (await IsValidAuthTypeAsync(user, request.GrantType))
{ {
var device = await _deviceValidator.SaveDeviceAsync(user, request); var device = await _deviceValidator.SaveDeviceAsync(user, request);
@ -174,8 +172,7 @@ public abstract class BaseRequestValidator<T> where T : class
await BuildErrorResultAsync("No device information provided.", false, context, user); await BuildErrorResultAsync("No device information provided.", false, context, user);
return; return;
} }
await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember);
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
} }
else else
{ {
@ -239,67 +236,6 @@ public abstract class BaseRequestValidator<T> where T : class
await SetSuccessResult(context, user, claims, customResponse); await SetSuccessResult(context, user, claims, customResponse);
} }
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
{
var providerKeys = new List<byte>();
var providers = new Dictionary<string, Dictionary<string, object>>();
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
if (organization?.GetTwoFactorProviders() != null)
{
enabledProviders.AddRange(organization.GetTwoFactorProviders().Where(
p => organization.TwoFactorProviderIsEnabled(p.Key)));
}
if (user.GetTwoFactorProviders() != null)
{
foreach (var p in user.GetTwoFactorProviders())
{
if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user))
{
enabledProviders.Add(p);
}
}
}
if (!enabledProviders.Any())
{
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
return;
}
foreach (var provider in enabledProviders)
{
providerKeys.Add((byte)provider.Key);
var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
providers.Add(((byte)provider.Key).ToString(), infoDict);
}
var twoFactorResultDict = new Dictionary<string, object>
{
{ "TwoFactorProviders", providers.Keys },
{ "TwoFactorProviders2", providers },
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) },
};
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
{
twoFactorResultDict.Add("SsoEmail2faSessionToken",
_tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user)));
twoFactorResultDict.Add("Email", user.Email);
}
SetTwoFactorResult(context, twoFactorResultDict);
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
{
// Send email now if this is their only 2FA method
await _userService.SendTwoFactorEmailAsync(user);
}
}
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
{ {
if (user != null) if (user != null)
@ -330,35 +266,13 @@ public abstract class BaseRequestValidator<T> where T : class
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse); protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
protected abstract ClaimsPrincipal GetSubject(T context); protected abstract ClaimsPrincipal GetSubject(T context);
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) /// <summary>
{ /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
if (request.GrantType == "client_credentials") /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
{ /// </summary>
// Do not require MFA for api key logins /// <param name="user">user trying to login</param>
return new Tuple<bool, Organization>(false, null); /// <param name="grantType">magic string identifying the grant type requested</param>
} /// <returns></returns>
var individualRequired = _userManager.SupportsUserTwoFactor &&
await _userManager.GetTwoFactorEnabledAsync(user) &&
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
Organization firstEnabledOrg = null;
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList();
if (orgs.Count > 0)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
if (twoFactorOrgs.Any())
{
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
firstEnabledOrg = userOrgs.FirstOrDefault(
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
}
}
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
}
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType) private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
{ {
if (grantType == "authorization_code" || grantType == "client_credentials") if (grantType == "authorization_code" || grantType == "client_credentials")
@ -368,7 +282,6 @@ public abstract class BaseRequestValidator<T> where T : class
return true; return true;
} }
// Check if user belongs to any organization with an active SSO policy // Check if user belongs to any organization with an active SSO policy
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
if (anySsoPoliciesApplicableToUser) if (anySsoPoliciesApplicableToUser)
@ -380,134 +293,6 @@ public abstract class BaseRequestValidator<T> where T : class
return true; return true;
} }
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
}
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
string token)
{
switch (type)
{
case TwoFactorProviderType.Authenticator:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.YubiKey:
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Remember:
if (type != TwoFactorProviderType.Remember &&
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return false;
}
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (type == TwoFactorProviderType.Duo)
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
}
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token);
case TwoFactorProviderType.OrganizationDuo:
if (!organization?.TwoFactorProviderIsEnabled(type) ?? true)
{
return false;
}
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (type == TwoFactorProviderType.OrganizationDuo)
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
}
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
default:
return false;
}
}
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
TwoFactorProviderType type, TwoFactorProvider provider)
{
switch (type)
{
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.YubiKey:
if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return null;
}
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type));
if (type == TwoFactorProviderType.Duo)
{
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user),
};
return duoResponse;
}
else if (type == TwoFactorProviderType.WebAuthn)
{
if (token == null)
{
return null;
}
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
}
else if (type == TwoFactorProviderType.Email)
{
var twoFactorEmail = (string)provider.MetaData["Email"];
var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);
return new Dictionary<string, object> { ["Email"] = redactedEmail };
}
else if (type == TwoFactorProviderType.YubiKey)
{
return new Dictionary<string, object> { ["Nfc"] = (bool)provider.MetaData["Nfc"] };
}
return null;
case TwoFactorProviderType.OrganizationDuo:
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
{
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user),
};
return duoResponse;
}
return null;
default:
return null;
}
}
private async Task ResetFailedAuthDetailsAsync(User user) private async Task ResetFailedAuthDetailsAsync(User user)
{ {
// Early escape if db hit not necessary // Early escape if db hit not necessary

View File

@ -1,9 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -11,7 +9,6 @@ using Bit.Core.IdentityServer;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using HandlebarsDotNet; using HandlebarsDotNet;
@ -20,7 +17,7 @@ using Microsoft.AspNetCore.Identity;
#nullable enable #nullable enable
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer.RequestValidators;
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>, public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
ICustomTokenRequestValidator ICustomTokenRequestValidator
@ -29,28 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
public CustomTokenRequestValidator( public CustomTokenRequestValidator(
UserManager<User> userManager, UserManager<User> userManager,
IDeviceValidator deviceValidator,
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, IDeviceValidator deviceValidator,
ITemporaryDuoWebV4SDKService duoWebV4SDKService, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger<CustomTokenRequestValidator> logger, ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) ISsoConfigRepository ssoConfigRepository,
: base(userManager, userService, eventService, deviceValidator, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, )
applicationCacheService, mailService, logger, currentContext, globalSettings, : base(
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userManager,
userService,
eventService,
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,
userRepository,
policyService,
featureService,
ssoConfigRepository,
userDecryptionOptionsBuilder) userDecryptionOptionsBuilder)
{ {
_userManager = userManager; _userManager = userManager;
@ -70,7 +75,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
} }
} }
string[] allowedGrantTypes = { "authorization_code", "client_credentials" }; string[] allowedGrantTypes = ["authorization_code", "client_credentials"];
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType) if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization") || context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation") || context.Result.ValidatedRequest.ClientId.StartsWith("installation")

View File

@ -8,7 +8,7 @@ using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer.RequestValidators;
public interface IDeviceValidator public interface IDeviceValidator
{ {
@ -41,6 +41,12 @@ public class DeviceValidator(
private readonly IMailService _mailService = mailService; private readonly IMailService _mailService = mailService;
private readonly ICurrentContext _currentContext = currentContext; private readonly ICurrentContext _currentContext = currentContext;
/// <summary>
/// Save a device to the database. If the device is already known, it will be returned.
/// </summary>
/// <param name="user">The user is assumed NOT null, still going to check though</param>
/// <param name="request">Duende Validated Request that contains the data to create the device object</param>
/// <returns>Returns null if user or device is malformed; The existing device if already in DB; a new device login</returns>
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request) public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
{ {
var device = GetDeviceFromRequest(request); var device = GetDeviceFromRequest(request);

View File

@ -1,8 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -10,13 +8,12 @@ using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer.RequestValidators;
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>, public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
IResourceOwnerPasswordValidator IResourceOwnerPasswordValidator
@ -31,11 +28,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger<ResourceOwnerPasswordValidator> logger, ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
@ -44,14 +38,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IAuthRequestRepository authRequestRepository, IAuthRequestRepository authRequestRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, userService, eventService, deviceValidator, : base(
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, userManager,
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, userService,
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) eventService,
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,
userRepository,
policyService,
featureService,
ssoConfigRepository,
userDecryptionOptionsBuilder)
{ {
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;

View File

@ -0,0 +1,297 @@

using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer.RequestValidators;
public interface ITwoFactorAuthenticationValidator
{
/// <summary>
/// Check if the user is required to use two-factor authentication to login. This is based on the user's
/// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type.
/// Client credentials and webauthn grant types do not require two-factor authentication.
/// </summary>
/// <param name="user">the active user for the request</param>
/// <param name="request">the request that contains the grant types</param>
/// <returns>boolean</returns>
Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);
/// <summary>
/// Builds the two-factor authentication result for the user based on the available two-factor providers
/// from either their user account or Organization.
/// </summary>
/// <param name="user">user trying to login</param>
/// <param name="organization">organization associated with the user; Can be null</param>
/// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>
Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);
/// <summary>
/// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses
/// organization duo, it will use the organization duo token provider to verify the token.
/// </summary>
/// <param name="user">the active User</param>
/// <param name="organization">organization of user; can be null</param>
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
/// <returns>boolean</returns>
Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
}
public class TwoFactorAuthenticationValidator(
IUserService userService,
UserManager<User> userManager,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IFeatureService featureService,
IApplicationCacheService applicationCacheService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
ICurrentContext currentContext) : ITwoFactorAuthenticationValidator
{
private readonly IUserService _userService = userService;
private readonly UserManager<User> _userManager = userManager;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService;
private readonly IFeatureService _featureService = featureService;
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository = organizationRepository;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;
private readonly ICurrentContext _currentContext = currentContext;
public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
{
if (request.GrantType == "client_credentials" || request.GrantType == "webauthn")
{
/*
Do not require MFA for api key logins.
We consider Fido2 userVerification a second factor, so we don't require a second factor here.
*/
return new Tuple<bool, Organization>(false, null);
}
var individualRequired = _userManager.SupportsUserTwoFactor &&
await _userManager.GetTwoFactorEnabledAsync(user) &&
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
Organization firstEnabledOrg = null;
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList();
if (orgs.Count > 0)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
if (twoFactorOrgs.Any())
{
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
firstEnabledOrg = userOrgs.FirstOrDefault(
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
}
}
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
}
public async Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization)
{
var enabledProviders = await GetEnabledTwoFactorProvidersAsync(user, organization);
if (enabledProviders.Count == 0)
{
return null;
}
var providers = new Dictionary<string, Dictionary<string, object>>();
foreach (var provider in enabledProviders)
{
var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
providers.Add(((byte)provider.Key).ToString(), twoFactorParams);
}
var twoFactorResultDict = new Dictionary<string, object>
{
{ "TwoFactorProviders", null },
{ "TwoFactorProviders2", providers }, // backwards compatibility
};
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
{
twoFactorResultDict.Add("SsoEmail2faSessionToken",
_ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user)));
twoFactorResultDict.Add("Email", user.Email);
}
if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
{
// Send email now if this is their only 2FA method
await _userService.SendTwoFactorEmailAsync(user);
}
return twoFactorResultDict;
}
public async Task<bool> VerifyTwoFactor(
User user,
Organization organization,
TwoFactorProviderType type,
string token)
{
if (organization != null && type == TwoFactorProviderType.OrganizationDuo)
{
if (organization.TwoFactorProviderIsEnabled(type))
{
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
}
return false;
}
switch (type)
{
case TwoFactorProviderType.Authenticator:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.YubiKey:
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Remember:
if (type != TwoFactorProviderType.Remember &&
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return false;
}
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (type == TwoFactorProviderType.Duo)
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
}
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token);
default:
return false;
}
}
private async Task<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(
User user, Organization organization)
{
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
var organizationTwoFactorProviders = organization?.GetTwoFactorProviders();
if (organizationTwoFactorProviders != null)
{
enabledProviders.AddRange(
organizationTwoFactorProviders.Where(
p => (p.Value?.Enabled ?? false) && organization.Use2fa));
}
var userTwoFactorProviders = user.GetTwoFactorProviders();
var userCanAccessPremium = await _userService.CanAccessPremium(user);
if (userTwoFactorProviders != null)
{
enabledProviders.AddRange(
userTwoFactorProviders.Where(p =>
// Providers that do not require premium
(p.Value.Enabled && !TwoFactorProvider.RequiresPremium(p.Key)) ||
// Providers that require premium and the User has Premium
(p.Value.Enabled && TwoFactorProvider.RequiresPremium(p.Key) && userCanAccessPremium)));
}
return enabledProviders;
}
/// <summary>
/// Builds the parameters for the two-factor authentication
/// </summary>
/// <param name="organization">We need the organization for Organization Duo Provider type</param>
/// <param name="user">The user for which the token is being generated</param>
/// <param name="type">Provider Type</param>
/// <param name="provider">Raw data that is used to create the response</param>
/// <returns>a dictionary with the correct provider configuration or null if the provider is not configured properly</returns>
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
TwoFactorProviderType type, TwoFactorProvider provider)
{
// We will always return this dictionary. If none of the criteria is met then it will return null.
var twoFactorParams = new Dictionary<string, object>();
// OrganizationDuo is odd since it doesn't use the UserManager built-in TwoFactor flows
/*
Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class
in the future the `AuthUrl` will be the generated "token" - PM-8107
*/
if (type == TwoFactorProviderType.OrganizationDuo &&
await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
{
twoFactorParams.Add("Host", provider.MetaData["Host"]);
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
return twoFactorParams;
}
// Individual 2FA providers use the UserManager built-in TwoFactor flow so we can generate the token before building the params
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type));
switch (type)
{
/*
Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class
in the future the `AuthUrl` will be the generated "token" - PM-8107
*/
case TwoFactorProviderType.Duo:
twoFactorParams.Add("Host", provider.MetaData["Host"]);
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
break;
case TwoFactorProviderType.WebAuthn:
if (token != null)
{
twoFactorParams = JsonSerializer.Deserialize<Dictionary<string, object>>(token);
}
break;
case TwoFactorProviderType.Email:
var twoFactorEmail = (string)provider.MetaData["Email"];
var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);
twoFactorParams.Add("Email", redactedEmail);
break;
case TwoFactorProviderType.YubiKey:
twoFactorParams.Add("Nfc", (bool)provider.MetaData["Nfc"]);
break;
}
// return null if the dictionary is empty
return twoFactorParams.Count > 0 ? twoFactorParams : null;
}
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
}
}

View File

@ -1,10 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
@ -19,7 +17,7 @@ using Duende.IdentityServer.Validation;
using Fido2NetLib; using Fido2NetLib;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer.RequestValidators;
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
{ {
@ -34,11 +32,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger<CustomTokenRequestValidator> logger, ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
@ -46,16 +41,27 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector, IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IFeatureService featureService, IFeatureService featureService,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
) )
: base(userManager, userService, eventService, deviceValidator, : base(
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, userManager,
applicationCacheService, mailService, logger, currentContext, globalSettings, userService,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) eventService,
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,
userRepository,
policyService,
featureService,
ssoConfigRepository,
userDecryptionOptionsBuilder)
{ {
_assertionOptionsDataProtector = assertionOptionsDataProtector; _assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
@ -122,12 +128,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
return context.Result.Subject; return context.Result.Subject;
} }
protected override Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
{
// We consider Fido2 userVerification a second factor, so we don't require a second factor here.
return Task.FromResult(new Tuple<bool, Organization>(false, null));
}
protected override void SetTwoFactorResult(ExtensionGrantValidationContext context, protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,
Dictionary<string, object> customResponse) Dictionary<string, object> customResponse)
{ {

View File

@ -3,6 +3,7 @@ using Bit.Core.IdentityServer;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Services; using Duende.IdentityServer.Services;
@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>(); services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>(); services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
services.AddTransient<IDeviceValidator, DeviceValidator>(); services.AddTransient<IDeviceValidator, DeviceValidator>();
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
var identityServerBuilder = services var identityServerBuilder = services

View File

@ -196,8 +196,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
return results.SingleOrDefault(); return results.SingleOrDefault();
} }
} }
public async Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>> public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
GetDetailsByIdWithCollectionsAsync(Guid id)
{ {
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
@ -206,9 +205,9 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
new { Id = id }, new { Id = id },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
var user = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault(); var organizationUserUserDetails = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList(); var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
return new Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>(user, collections); return (organizationUserUserDetails, collections);
} }
} }

View File

@ -248,7 +248,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
} }
} }
public async Task<Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>> GetDetailsByIdWithCollectionsAsync(Guid id) public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
{ {
var organizationUserUserDetails = await GetDetailsByIdAsync(id); var organizationUserUserDetails = await GetDetailsByIdAsync(id);
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
@ -265,7 +265,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
HidePasswords = cu.HidePasswords, HidePasswords = cu.HidePasswords,
Manage = cu.Manage Manage = cu.Manage
}).ToListAsync(); }).ToListAsync();
return new Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>(organizationUserUserDetails, collections); return (organizationUserUserDetails, collections);
} }
} }

View File

@ -275,6 +275,11 @@ public static class ServiceCollectionExtensions
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation"); services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>(); services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
} }
else
{
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
{ {
@ -294,10 +299,6 @@ public static class ServiceCollectionExtensions
"implementation"); "implementation");
} }
} }
else
{
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
}
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString)) if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
{ {

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
@ -15,6 +16,7 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -185,14 +187,46 @@ public class OrganizationUsersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
} }
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task Get_ReturnsUser(
bool accountDeprovisioningEnabled,
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.Permissions = null;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(accountDeprovisioningEnabled);
sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByIdWithCollectionsAsync(organizationUser.Id)
.Returns((organizationUser, collections));
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
Assert.Equal(organizationUser.Id, response.Id);
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task Get_ReturnsUsers( public async Task GetMany_ReturnsUsers(
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility, ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider) SutProvider<OrganizationUsersController> sutProvider)
{ {
Get_Setup(organizationAbility, organizationUsers, sutProvider); GetMany_Setup(organizationAbility, organizationUsers, sutProvider);
var response = await sutProvider.Sut.Get(organizationAbility.Id); var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false);
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id))); Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
} }
@ -368,7 +402,7 @@ public class OrganizationUsersControllerTests
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model)); await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model));
} }
private void Get_Setup(OrganizationAbility organizationAbility, private void GetMany_Setup(OrganizationAbility organizationAbility,
ICollection<OrganizationUserUserDetails> organizationUsers, ICollection<OrganizationUserUserDetails> organizationUsers,
SutProvider<OrganizationUsersController> sutProvider) SutProvider<OrganizationUsersController> sutProvider)
{ {

View File

@ -2,7 +2,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -24,7 +23,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
CurrentContextOrganization organization, CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider) SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{ {
EnableFeatureFlag(sutProvider);
organization.Type = userType; organization.Type = userType;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -48,7 +46,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
CurrentContextOrganization organization, CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider) SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{ {
EnableFeatureFlag(sutProvider);
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ProviderUserForOrgAsync(organization.Id) .ProviderUserForOrgAsync(organization.Id)
@ -69,7 +66,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
CurrentContextOrganization organization, CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider) SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{ {
EnableFeatureFlag(sutProvider);
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
@ -88,78 +84,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
public async Task ReadAll_NotMember_NoSuccess( public async Task ReadAll_NotMember_NoSuccess(
CurrentContextOrganization organization, CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider) SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{
EnableFeatureFlag(sutProvider);
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id)
);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
private void EnableFeatureFlag(SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)
.Returns(true);
}
// TESTS WITH FLAG DISABLED - TO BE DELETED IN FLAG CLEANUP
[Theory, CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task FlagDisabled_ReadAll_AnyMemberOfOrg_Success(
OrganizationUserType userType,
Guid userId, SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider,
CurrentContextOrganization organization)
{
organization.Type = userType;
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id));
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CurrentContextOrganizationCustomize]
public async Task FlagDisabled_ReadAll_ProviderUser_Success(
CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{
organization.Type = OrganizationUserType.User;
sutProvider.GetDependency<ICurrentContext>()
.ProviderUserForOrgAsync(organization.Id)
.Returns(true);
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id));
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task FlagDisabled_ReadAll_NotMember_NoSuccess(
CurrentContextOrganization organization,
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
{ {
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserDetailsOperations.ReadAll }, new[] { OrganizationUserUserDetailsOperations.ReadAll },

View File

@ -0,0 +1,57 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
[SutProviderCustomize]
public class OrganizationHasVerifiedDomainsQueryTests
{
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue(
OrganizationDomain organizationDomain,
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(
OrganizationDomain organizationDomain,
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationId)
.Returns(new List<OrganizationDomain>());
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);
Assert.False(result);
}
}

View File

@ -1,4 +1,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -15,7 +18,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
public class VerifyOrganizationDomainCommandTests public class VerifyOrganizationDomainCommandTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -37,7 +40,7 @@ public class VerifyOrganizationDomainCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -61,7 +64,7 @@ public class VerifyOrganizationDomainCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -120,7 +123,7 @@ public class VerifyOrganizationDomainCommandTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider) public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var domain = new OrganizationDomain() var domain = new OrganizationDomain()
{ {
@ -137,4 +140,97 @@ public class VerifyOrganizationDomainCommandTests
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified, .LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification); EventSystemUser.DomainVerification);
} }
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
.Received(1)
.SaveAsync(Arg.Is<Policy>(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null);
}
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
}
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
}
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
}
} }

View File

@ -76,48 +76,4 @@ public class OrganizationDomainServiceTests
await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(1) await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(1)
.DeleteExpiredAsync(7); .DeleteExpiredAsync(7);
} }
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue(
OrganizationDomain organizationDomain,
SutProvider<OrganizationDomainService> sutProvider)
{
organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(
OrganizationDomain organizationDomain,
SutProvider<OrganizationDomainService> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationDomainService> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationId)
.Returns(new List<OrganizationDomain>());
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);
Assert.False(result);
}
} }

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
@ -815,4 +816,32 @@ public class PolicyServiceTests
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }
}); });
} }
[Theory, BitAutoData]
public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider<PolicyService> sutProvider)
{
org.Id = policy.OrganizationId;
org.UsePolicies = true;
policy.Enabled = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policy.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(org.Id)
.Returns(true);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, null));
Assert.Equal("Organization has verified domains.", badRequestException.Message);
}
} }

View File

@ -5,7 +5,7 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Identity.Models.Request.Accounts; using Bit.Identity.Models.Request.Accounts;
using Bit.IntegrationTestCommon.Factories; using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -237,6 +237,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
MasterPasswordHash = DefaultPassword MasterPasswordHash = DefaultPassword
}); });
var userManager = factory.GetService<UserManager<User>>(); var userManager = factory.GetService<UserManager<User>>();
await factory.RegisterAsync(new RegisterRequestModel
{
Email = DefaultUsername,
MasterPasswordHash = DefaultPassword
});
var user = await userManager.FindByEmailAsync(DefaultUsername); var user = await userManager.FindByEmailAsync(DefaultUsername);
Assert.NotNull(user); Assert.NotNull(user);

View File

@ -1,8 +1,7 @@
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -11,8 +10,8 @@ using Bit.Core.Models.Api;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Identity.Test.Wrappers; using Bit.Identity.Test.Wrappers;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
@ -32,18 +31,14 @@ public class BaseRequestValidatorTests
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IDeviceValidator _deviceValidator; private readonly IDeviceValidator _deviceValidator;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly ILogger<BaseRequestValidatorTests> _logger; private readonly ILogger<BaseRequestValidatorTests> _logger;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
@ -52,43 +47,35 @@ public class BaseRequestValidatorTests
public BaseRequestValidatorTests() public BaseRequestValidatorTests()
{ {
_userManager = SubstituteUserManager();
_userService = Substitute.For<IUserService>(); _userService = Substitute.For<IUserService>();
_eventService = Substitute.For<IEventService>(); _eventService = Substitute.For<IEventService>();
_deviceValidator = Substitute.For<IDeviceValidator>(); _deviceValidator = Substitute.For<IDeviceValidator>();
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>(); _twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
_duoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>(); _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_applicationCacheService = Substitute.For<IApplicationCacheService>();
_mailService = Substitute.For<IMailService>(); _mailService = Substitute.For<IMailService>();
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>(); _logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
_currentContext = Substitute.For<ICurrentContext>(); _currentContext = Substitute.For<ICurrentContext>();
_globalSettings = Substitute.For<GlobalSettings>(); _globalSettings = Substitute.For<GlobalSettings>();
_userRepository = Substitute.For<IUserRepository>(); _userRepository = Substitute.For<IUserRepository>();
_policyService = Substitute.For<IPolicyService>(); _policyService = Substitute.For<IPolicyService>();
_tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
_featureService = Substitute.For<IFeatureService>(); _featureService = Substitute.For<IFeatureService>();
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>(); _ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>(); _userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
_userManager = SubstituteUserManager();
_sut = new BaseRequestValidatorTestWrapper( _sut = new BaseRequestValidatorTestWrapper(
_userManager, _userManager,
_userService, _userService,
_eventService, _eventService,
_deviceValidator, _deviceValidator,
_organizationDuoWebTokenProvider, _twoFactorAuthenticationValidator,
_duoWebV4SDKService,
_organizationRepository,
_organizationUserRepository, _organizationUserRepository,
_applicationCacheService,
_mailService, _mailService,
_logger, _logger,
_currentContext, _currentContext,
_globalSettings, _globalSettings,
_userRepository, _userRepository,
_policyService, _policyService,
_tokenDataFactory,
_featureService, _featureService,
_ssoConfigRepository, _ssoConfigRepository,
_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder);
@ -202,6 +189,9 @@ public class BaseRequestValidatorTests
{ {
// Arrange // Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult); var context = CreateContext(tokenRequest, requestContext, grantResult);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, default)));
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true; _sut.isValid = true;
@ -230,6 +220,9 @@ public class BaseRequestValidatorTests
{ {
// Arrange // Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult); var context = CreateContext(tokenRequest, requestContext, grantResult);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true; _sut.isValid = true;
@ -271,6 +264,9 @@ public class BaseRequestValidatorTests
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>()) _deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device); .Returns(device);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// Act // Act
await _sut.ValidateAsync(context); await _sut.ValidateAsync(context);
@ -306,6 +302,9 @@ public class BaseRequestValidatorTests
_policyService.AnyPoliciesApplicableToUserAsync( _policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true)); .Returns(Task.FromResult(true));
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// Act // Act
await _sut.ValidateAsync(context); await _sut.ValidateAsync(context);
@ -330,6 +329,9 @@ public class BaseRequestValidatorTests
context.ValidatedTokenRequest.ClientId = "Not Web"; context.ValidatedTokenRequest.ClientId = "Not Web";
_sut.isValid = true; _sut.isValid = true;
_featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// Act // Act
await _sut.ValidateAsync(context); await _sut.ValidateAsync(context);
@ -341,28 +343,6 @@ public class BaseRequestValidatorTests
, errorResponse.Message); , errorResponse.Message);
} }
[Theory, BitAutoData]
public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
context.ValidatedTokenRequest.GrantType = "client_credentials";
// Act
var result = await _sut.TestRequiresTwoFactorAsync(
context.CustomValidatorRequestContext.User,
context.ValidatedTokenRequest);
// Assert
Assert.False(result.Item1);
Assert.Null(result.Item2);
}
private BaseRequestValidationContextFake CreateContext( private BaseRequestValidationContextFake CreateContext(
ValidatedTokenRequest tokenRequest, ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext, CustomValidatorRequestContext requestContext,

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using NSubstitute; using NSubstitute;

View File

@ -0,0 +1,575 @@
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Identity.Test.Wrappers;
using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
using AuthFixtures = Bit.Identity.Test.AutoFixture;
namespace Bit.Identity.Test.IdentityServer;
public class TwoFactorAuthenticationValidatorTests
{
private readonly IUserService _userService;
private readonly UserManagerTestWrapper<User> _userManager;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService;
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
private readonly ICurrentContext _currentContext;
private readonly TwoFactorAuthenticationValidator _sut;
public TwoFactorAuthenticationValidatorTests()
{
_userService = Substitute.For<IUserService>();
_userManager = SubstituteUserManager();
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
_temporaryDuoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
_featureService = Substitute.For<IFeatureService>();
_applicationCacheService = Substitute.For<IApplicationCacheService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
_currentContext = Substitute.For<ICurrentContext>();
_sut = new TwoFactorAuthenticationValidator(
_userService,
_userManager,
_organizationDuoWebTokenProvider,
_temporaryDuoWebV4SDKService,
_featureService,
_applicationCacheService,
_organizationUserRepository,
_organizationRepository,
_ssoEmail2faSessionTokenable,
_currentContext);
}
[Theory]
[BitAutoData("password")]
[BitAutoData("authorization_code")]
public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request.GrantType = grantType;
// All three of these must be true for the two factor authentication to be required
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.SUPPORTS_TWO_FACTOR = true;
// In order for the two factor authentication to be required, the user must have at least one two factor provider
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
// Act
var result = await _sut.RequiresTwoFactorAsync(user, request);
// Assert
Assert.True(result.Item1);
Assert.Null(result.Item2);
}
[Theory]
[BitAutoData("client_credentials")]
[BitAutoData("webauthn")]
public async void RequiresTwoFactorAsync_NotRequired_ReturnFalse(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request.GrantType = grantType;
// Act
var result = await _sut.RequiresTwoFactorAsync(user, request);
// Assert
Assert.False(result.Item1);
Assert.Null(result.Item2);
}
[Theory]
[BitAutoData("password")]
[BitAutoData("authorization_code")]
public async void RequiresTwoFactorAsync_IndividualFalse_OrganizationRequired_ReturnTrue(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user,
OrganizationUserOrganizationDetails orgUser,
Organization organization,
ICollection<CurrentContextOrganization> organizationCollection)
{
// Arrange
request.GrantType = grantType;
// Link the orgUser to the User making the request
orgUser.UserId = user.Id;
// Link organization to the organization user
organization.Id = orgUser.OrganizationId;
// Set Organization 2FA to required
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
// Make sure organization list is not empty
organizationCollection.Clear();
// Fix OrganizationUser Permissions field
orgUser.Permissions = "{}";
organizationCollection.Add(new CurrentContextOrganization(orgUser));
_currentContext.OrganizationMembershipAsync(Arg.Any<IOrganizationUserRepository>(), Arg.Any<Guid>())
.Returns(Task.FromResult(organizationCollection));
_applicationCacheService.GetOrganizationAbilitiesAsync()
.Returns(new Dictionary<Guid, OrganizationAbility>()
{
{ orgUser.OrganizationId, new OrganizationAbility(organization)}
});
_organizationRepository.GetManyByUserIdAsync(Arg.Any<Guid>()).Returns([organization]);
// Act
var result = await _sut.RequiresTwoFactorAsync(user, request);
// Assert
Assert.True(result.Item1);
Assert.NotNull(result.Item2);
Assert.IsType<Organization>(result.Item2);
}
[Theory]
[BitAutoData]
public async void BuildTwoFactorResultAsync_NoProviders_ReturnsNull(
User user,
Organization organization)
{
// Arrange
organization.Use2fa = true;
organization.TwoFactorProviders = "{}";
organization.Enabled = true;
user.TwoFactorProviders = "";
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void BuildTwoFactorResultAsync_OrganizationProviders_NotEnabled_ReturnsNull(
User user,
Organization organization)
{
// Arrange
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationNotEnabledDuoProviderJson();
organization.Enabled = true;
user.TwoFactorProviders = null;
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull(
User user,
Organization organization)
{
// Arrange
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
user.TwoFactorProviders = null;
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
// Assert
Assert.NotNull(result);
Assert.IsType<Dictionary<string, object>>(result);
Assert.NotEmpty(result);
Assert.True(result.ContainsKey("TwoFactorProviders2"));
}
[Theory]
[BitAutoData]
public async void BuildTwoFactorResultAsync_IndividualProviders_NotEnabled_ReturnsNull(
User user)
{
// Arrange
user.TwoFactorProviders = GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType.Email);
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, null);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull(
User user)
{
// Arrange
_userService.CanAccessPremium(user).Returns(true);
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(TwoFactorProviderType.Duo);
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, null);
// Assert
Assert.NotNull(result);
Assert.IsType<Dictionary<string, object>>(result);
Assert.NotEmpty(result);
Assert.True(result.ContainsKey("TwoFactorProviders2"));
}
[Theory]
[BitAutoData(TwoFactorProviderType.Email)]
public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull(
TwoFactorProviderType providerType,
User user)
{
// Arrange
var providerTypeInt = (int)providerType;
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.SUPPORTS_TWO_FACTOR = true;
_userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()];
_userService.TwoFactorProviderIsEnabledAsync(Arg.Any<TwoFactorProviderType>(), user)
.Returns(true);
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, null);
// Assert
Assert.NotNull(result);
Assert.IsType<Dictionary<string, object>>(result);
Assert.NotEmpty(result);
Assert.True(result.ContainsKey("TwoFactorProviders2"));
var providers = (Dictionary<string, Dictionary<string, object>>)result["TwoFactorProviders2"];
Assert.True(providers.ContainsKey(providerTypeInt.ToString()));
Assert.True(result.ContainsKey("SsoEmail2faSessionToken"));
Assert.True(result.ContainsKey("Email"));
await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any<User>());
}
[Theory]
[BitAutoData(TwoFactorProviderType.Duo)]
[BitAutoData(TwoFactorProviderType.WebAuthn)]
[BitAutoData(TwoFactorProviderType.Email)]
[BitAutoData(TwoFactorProviderType.YubiKey)]
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
public async void BuildTwoFactorResultAsync_IndividualProvider_ReturnMatchesType(
TwoFactorProviderType providerType,
User user)
{
// Arrange
var providerTypeInt = (int)providerType;
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.SUPPORTS_TWO_FACTOR = true;
_userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()];
_userManager.TWO_FACTOR_TOKEN = "{\"Key1\":\"WebauthnToken\"}";
_userService.CanAccessPremium(user).Returns(true);
// Act
var result = await _sut.BuildTwoFactorResultAsync(user, null);
// Assert
Assert.NotNull(result);
Assert.IsType<Dictionary<string, object>>(result);
Assert.NotEmpty(result);
Assert.True(result.ContainsKey("TwoFactorProviders2"));
var providers = (Dictionary<string, Dictionary<string, object>>)result["TwoFactorProviders2"];
Assert.True(providers.ContainsKey(providerTypeInt.ToString()));
}
[Theory]
[BitAutoData]
public async void VerifyTwoFactorAsync_Individual_TypeNull_ReturnsFalse(
User user,
string token)
{
// Arrange
_userService.TwoFactorProviderIsEnabledAsync(
TwoFactorProviderType.Email, user).Returns(true);
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
// Act
var result = await _sut.VerifyTwoFactor(
user, null, TwoFactorProviderType.U2f, token);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void VerifyTwoFactorAsync_Individual_NotEnabled_ReturnsFalse(
User user,
string token)
{
// Arrange
_userService.TwoFactorProviderIsEnabledAsync(
TwoFactorProviderType.Email, user).Returns(false);
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
// Act
var result = await _sut.VerifyTwoFactor(
user, null, TwoFactorProviderType.Email, token);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void VerifyTwoFactorAsync_Organization_NotEnabled_ReturnsFalse(
User user,
string token)
{
// Arrange
_userService.TwoFactorProviderIsEnabledAsync(
TwoFactorProviderType.OrganizationDuo, user).Returns(false);
_userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"];
// Act
var result = await _sut.VerifyTwoFactor(
user, null, TwoFactorProviderType.OrganizationDuo, token);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.Duo)]
[BitAutoData(TwoFactorProviderType.WebAuthn)]
[BitAutoData(TwoFactorProviderType.Email)]
[BitAutoData(TwoFactorProviderType.YubiKey)]
[BitAutoData(TwoFactorProviderType.Remember)]
public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue(
TwoFactorProviderType providerType,
User user,
string token)
{
// Arrange
_userService.TwoFactorProviderIsEnabledAsync(
providerType, user).Returns(true);
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
// Act
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.Duo)]
[BitAutoData(TwoFactorProviderType.WebAuthn)]
[BitAutoData(TwoFactorProviderType.Email)]
[BitAutoData(TwoFactorProviderType.YubiKey)]
[BitAutoData(TwoFactorProviderType.Remember)]
public async void VerifyTwoFactorAsync_Individual_InvalidToken_ReturnsFalse(
TwoFactorProviderType providerType,
User user,
string token)
{
// Arrange
_userService.TwoFactorProviderIsEnabledAsync(
providerType, user).Returns(true);
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.TWO_FACTOR_TOKEN_VERIFIED = false;
// Act
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue(
TwoFactorProviderType providerType,
User user,
Organization organization,
string token)
{
// Arrange
_organizationDuoWebTokenProvider.ValidateAsync(
token, organization, user).Returns(true);
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
// Act
var result = await _sut.VerifyTwoFactor(
user, organization, providerType, token);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.Duo)]
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue(
TwoFactorProviderType providerType,
User user,
Organization organization,
string token)
{
// Arrange
_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true);
_userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true);
_temporaryDuoWebV4SDKService.ValidateAsync(
token, Arg.Any<TwoFactorProvider>(), user).Returns(true);
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.TWO_FACTOR_TOKEN = token;
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
// Act
var result = await _sut.VerifyTwoFactor(
user, organization, providerType, token);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData(TwoFactorProviderType.Duo)]
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
public async void VerifyTwoFactorAsync_TemporaryDuoService_InvalidToken_ReturnsFalse(
TwoFactorProviderType providerType,
User user,
Organization organization,
string token)
{
// Arrange
_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true);
_userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true);
_temporaryDuoWebV4SDKService.ValidateAsync(
token, Arg.Any<TwoFactorProvider>(), user).Returns(true);
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
organization.Use2fa = true;
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
_userManager.TWO_FACTOR_ENABLED = true;
_userManager.TWO_FACTOR_TOKEN = token;
_userManager.TWO_FACTOR_TOKEN_VERIFIED = false;
// Act
var result = await _sut.VerifyTwoFactor(
user, organization, providerType, token);
// Assert
Assert.True(result);
}
private static UserManagerTestWrapper<User> SubstituteUserManager()
{
return new UserManagerTestWrapper<User>(
Substitute.For<IUserTwoFactorStore<User>>(),
Substitute.For<IOptions<IdentityOptions>>(),
Substitute.For<IPasswordHasher<User>>(),
Enumerable.Empty<IUserValidator<User>>(),
Enumerable.Empty<IPasswordValidator<User>>(),
Substitute.For<ILookupNormalizer>(),
Substitute.For<IdentityErrorDescriber>(),
Substitute.For<IServiceProvider>(),
Substitute.For<ILogger<UserManager<User>>>());
}
private static string GetTwoFactorOrganizationDuoProviderJson(bool enabled = true)
{
return
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
}
private static string GetTwoFactorOrganizationNotEnabledDuoProviderJson(bool enabled = true)
{
return
"{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
}
private static string GetTwoFactorIndividualProviderJson(TwoFactorProviderType providerType)
{
return providerType switch
{
TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}",
TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":true,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}",
TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":true,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}",
TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
_ => "{}",
};
}
private static string GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType providerType)
{
return providerType switch
{
TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":false,\"MetaData\":{\"Email\":\"user@test.dev\"}}}",
TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":false,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}",
TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":false,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}",
TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
_ => "{}",
};
}
}

View File

@ -1,16 +1,13 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer;
using Bit.Identity.IdentityServer.RequestValidators;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -54,18 +51,14 @@ IBaseRequestValidatorTestWrapper
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger logger, ILogger logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
@ -74,18 +67,14 @@ IBaseRequestValidatorTestWrapper
userService, userService,
eventService, eventService,
deviceValidator, deviceValidator,
organizationDuoWebTokenProvider, twoFactorAuthenticationValidator,
duoWebV4SDKService,
organizationRepository,
organizationUserRepository, organizationUserRepository,
applicationCacheService,
mailService, mailService,
logger, logger,
currentContext, currentContext,
globalSettings, globalSettings,
userRepository, userRepository,
policyService, policyService,
tokenDataFactory,
featureService, featureService,
ssoConfigRepository, ssoConfigRepository,
userDecryptionOptionsBuilder) userDecryptionOptionsBuilder)
@ -98,13 +87,6 @@ IBaseRequestValidatorTestWrapper
await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext); await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext);
} }
public async Task<Tuple<bool, Organization>> TestRequiresTwoFactorAsync(
User user,
ValidatedTokenRequest context)
{
return await RequiresTwoFactorAsync(user, context);
}
protected override ClaimsPrincipal GetSubject( protected override ClaimsPrincipal GetSubject(
BaseRequestValidationContextFake context) BaseRequestValidationContextFake context)
{ {

View File

@ -0,0 +1,96 @@

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Bit.Identity.Test.Wrappers;
public class UserManagerTestWrapper<TUser> : UserManager<TUser> where TUser : class
{
/// <summary>
/// Modify this value to mock the responses from UserManager.GetTwoFactorEnabledAsync()
/// </summary>
public bool TWO_FACTOR_ENABLED { get; set; } = false;
/// <summary>
/// Modify this value to mock the responses from UserManager.GetValidTwoFactorProvidersAsync()
/// </summary>
public IList<string> TWO_FACTOR_PROVIDERS { get; set; } = [];
/// <summary>
/// Modify this value to mock the responses from UserManager.GenerateTwoFactorTokenAsync()
/// </summary>
public string TWO_FACTOR_TOKEN { get; set; } = string.Empty;
/// <summary>
/// Modify this value to mock the responses from UserManager.VerifyTwoFactorTokenAsync()
/// </summary>
public bool TWO_FACTOR_TOKEN_VERIFIED { get; set; } = false;
/// <summary>
/// Modify this value to mock the responses from UserManager.SupportsUserTwoFactor
/// </summary>
public bool SUPPORTS_TWO_FACTOR { get; set; } = false;
public override bool SupportsUserTwoFactor
{
get
{
return SUPPORTS_TWO_FACTOR;
}
}
public UserManagerTestWrapper(
IUserStore<TUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators,
keyNormalizer, errors, services, logger)
{ }
/// <summary>
/// return class variable TWO_FACTOR_ENABLED
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public override async Task<bool> GetTwoFactorEnabledAsync(TUser user)
{
return TWO_FACTOR_ENABLED;
}
/// <summary>
/// return class variable TWO_FACTOR_PROVIDERS
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public override async Task<IList<string>> GetValidTwoFactorProvidersAsync(TUser user)
{
return TWO_FACTOR_PROVIDERS;
}
/// <summary>
/// return class variable TWO_FACTOR_TOKEN
/// </summary>
/// <param name="user"></param>
/// <param name="tokenProvider"></param>
/// <returns></returns>
public override async Task<string> GenerateTwoFactorTokenAsync(TUser user, string tokenProvider)
{
return TWO_FACTOR_TOKEN;
}
/// <summary>
/// return class variable TWO_FACTOR_TOKEN_VERIFIED
/// </summary>
/// <param name="user"></param>
/// <param name="tokenProvider"></param>
/// <param name="token"></param>
/// <returns></returns>
public override async Task<bool> VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token)
{
return TWO_FACTOR_TOKEN_VERIFIED;
}
}