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:
commit
89b0848002
52
.github/workflows/repository-management.yml
vendored
52
.github/workflows/repository-management.yml
vendored
@ -135,13 +135,61 @@ jobs:
|
||||
git config --local user.email "actions@github.com"
|
||||
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
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
|
||||
- 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: |
|
||||
git pull -pt
|
||||
git push
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--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:
|
||||
|
@ -379,42 +379,23 @@ public class ProviderBillingService(
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||
|
||||
var teamsProviderPlan =
|
||||
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
||||
|
||||
if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id);
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
|
||||
throw new BillingException();
|
||||
if (!providerPlan.IsConfigured())
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = providerPlan.SeatMinimum
|
||||
});
|
||||
}
|
||||
|
||||
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = teamsProviderPlan.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
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
|
@ -53,6 +53,8 @@ public class OrganizationUsersController : Controller
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -73,7 +75,9 @@ public class OrganizationUsersController : Controller
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -94,29 +98,34 @@ public class OrganizationUsersController : Controller
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[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));
|
||||
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId))
|
||||
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
|
||||
{
|
||||
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)
|
||||
{
|
||||
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id);
|
||||
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpGet("mini-details")]
|
||||
[RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)]
|
||||
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid 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 organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id));
|
||||
var responses = organizationUsers
|
||||
.Select(o =>
|
||||
{
|
||||
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;
|
||||
});
|
||||
@ -682,4 +693,15 @@ public class OrganizationUsersController : Controller
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
|
||||
{
|
||||
public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser,
|
||||
public OrganizationUserDetailsResponseModel(
|
||||
OrganizationUser organizationUser,
|
||||
bool managedByOrganization,
|
||||
IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(organizationUser, "organizationUserDetails")
|
||||
{
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||
}
|
||||
|
||||
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
bool managedByOrganization,
|
||||
IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(organizationUser, "organizationUserDetails")
|
||||
{
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||
}
|
||||
|
||||
public bool ManagedByOrganization { get; set; }
|
||||
|
||||
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||
{
|
||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
bool twoFactorEnabled, string obj = "organizationUserUserDetails")
|
||||
bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails")
|
||||
: base(organizationUser, obj)
|
||||
{
|
||||
if (organizationUser == null)
|
||||
@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
||||
Groups = organizationUser.Groups;
|
||||
// Prevent reset password when using key connector.
|
||||
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
||||
public string AvatarColor { get; set; }
|
||||
public bool TwoFactorEnabled { 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<Guid> Groups { get; set; }
|
||||
}
|
||||
|
@ -71,14 +71,13 @@ public class MembersController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
var orgUser = userDetails?.Item1;
|
||||
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
userDetails.Item2);
|
||||
collections);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
|
||||
public interface IOrganizationHasVerifiedDomainsQuery
|
||||
{
|
||||
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationService organizationService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_organizationService = organizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
{
|
||||
domain.SetVerifiedDate();
|
||||
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
@ -10,12 +9,10 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
||||
: AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext, IFeatureService featureService)
|
||||
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
@ -37,29 +34,6 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
||||
}
|
||||
|
||||
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
|
||||
var organization = _currentContext.GetOrganization(organizationId);
|
||||
|
@ -22,8 +22,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
|
||||
Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
||||
GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||
Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null);
|
||||
|
@ -4,8 +4,4 @@ public interface IOrganizationDomainService
|
||||
{
|
||||
Task ValidateOrganizationsDomainAsync();
|
||||
Task OrganizationDomainMaintenanceAsync();
|
||||
/// <summary>
|
||||
/// Indicates if the organization has any verified domains.
|
||||
/// </summary>
|
||||
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
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.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
@ -32,6 +33,7 @@ public class PolicyService : IPolicyService
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
@ -45,7 +47,8 @@ public class PolicyService : IPolicyService
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
@ -59,6 +62,7 @@ public class PolicyService : IPolicyService
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||
@ -239,6 +243,7 @@ public class PolicyService : IPolicyService
|
||||
case PolicyType.SingleOrg:
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await HasVerifiedDomainsAsync(org);
|
||||
await RequiredBySsoAsync(org);
|
||||
await RequiredByVaultTimeoutAsync(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)
|
||||
{
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
|
@ -87,7 +87,9 @@ public record EnterprisePlan : Plan
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
|
||||
SeatPrice = 72;
|
||||
ProviderPortalSeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -141,7 +141,6 @@ public static class FeatureFlagKeys
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
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 AccessIntelligence = "pm-13227-access-intelligence";
|
||||
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 LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.24" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.34" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.30" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
|
||||
@ -36,7 +36,7 @@
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<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.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||
@ -59,7 +59,7 @@
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<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>
|
||||
|
@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
|
||||
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
|
||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||
services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
@ -1,15 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
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.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -17,32 +12,26 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
|
||||
protected ICurrentContext CurrentContext { get; }
|
||||
protected IPolicyService PolicyService { get; }
|
||||
@ -56,18 +45,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
@ -76,18 +61,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_deviceValidator = deviceValidator;
|
||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
_duoWebV4SDKService = duoWebV4SDKService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
CurrentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
PolicyService = policyService;
|
||||
_userRepository = userRepository;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
FeatureService = featureService;
|
||||
SsoConfigRepository = ssoConfigRepository;
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
@ -104,12 +85,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
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 user = validatorContext.User;
|
||||
if (!valid)
|
||||
@ -123,17 +98,37 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
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 (!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;
|
||||
}
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
return;
|
||||
}
|
||||
|
||||
var verified = await VerifyTwoFactor(user, twoFactorOrganization,
|
||||
twoFactorProviderType, twoFactorToken);
|
||||
var verified = await _twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
|
||||
|
||||
// 2FA required but request not valid or remember token expired response
|
||||
if (!verified || isBot)
|
||||
{
|
||||
if (twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
@ -143,16 +138,20 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
twoFactorRequest = false;
|
||||
validTwoFactorRequest = false;
|
||||
twoFactorRemember = false;
|
||||
twoFactorToken = null;
|
||||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
||||
await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -239,67 +236,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
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)
|
||||
{
|
||||
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 ClaimsPrincipal GetSubject(T context);
|
||||
|
||||
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (request.GrantType == "client_credentials")
|
||||
{
|
||||
// Do not require MFA for api key logins
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
|
||||
/// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
|
||||
/// </summary>
|
||||
/// <param name="user">user trying to login</param>
|
||||
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
||||
{
|
||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||
@ -368,7 +282,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (anySsoPoliciesApplicableToUser)
|
||||
@ -380,134 +293,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
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)
|
||||
{
|
||||
// Early escape if db hit not necessary
|
@ -1,9 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -11,7 +9,6 @@ using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using HandlebarsDotNet;
|
||||
@ -20,7 +17,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
ICustomTokenRequestValidator
|
||||
@ -29,28 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceValidator deviceValidator,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
|
||||
)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_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)
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|
@ -8,7 +8,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public interface IDeviceValidator
|
||||
{
|
||||
@ -41,6 +41,12 @@ public class DeviceValidator(
|
||||
private readonly IMailService _mailService = mailService;
|
||||
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)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
@ -1,8 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
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.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -10,13 +8,12 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
||||
IResourceOwnerPasswordValidator
|
||||
@ -31,11 +28,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@ -44,14 +38,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
|
||||
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
@ -19,7 +17,7 @@ using Duende.IdentityServer.Validation;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
|
||||
{
|
||||
@ -34,11 +32,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@ -46,16 +41,27 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
||||
)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
@ -122,12 +128,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
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,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
@ -3,6 +3,7 @@ using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityServer.ResponseHandling;
|
||||
using Duende.IdentityServer.Services;
|
||||
@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||
|
||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||
var identityServerBuilder = services
|
||||
|
@ -196,8 +196,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
public async Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
||||
GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@ -206,9 +205,9 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
new { Id = id },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var user = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
||||
var organizationUserUserDetails = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
||||
var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
||||
return new Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>(user, collections);
|
||||
return (organizationUserUserDetails, collections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
@ -265,7 +265,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToListAsync();
|
||||
return new Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>(organizationUserUserDetails, collections);
|
||||
return (organizationUserUserDetails, collections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,6 +275,11 @@ public static class ServiceCollectionExtensions
|
||||
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
}
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||
{
|
||||
@ -294,10 +299,6 @@ public static class ServiceCollectionExtensions
|
||||
"implementation");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
@ -15,6 +16,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -185,14 +187,46 @@ public class OrganizationUsersControllerTests
|
||||
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]
|
||||
[BitAutoData]
|
||||
public async Task Get_ReturnsUsers(
|
||||
public async Task GetMany_ReturnsUsers(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
Get_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id);
|
||||
GetMany_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private void Get_Setup(OrganizationAbility organizationAbility,
|
||||
private void GetMany_Setup(OrganizationAbility organizationAbility,
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -24,7 +23,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = userType;
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
|
||||
@ -48,7 +46,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = OrganizationUserType.User;
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUserForOrgAsync(organization.Id)
|
||||
@ -69,7 +66,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = OrganizationUserType.User;
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
@ -88,78 +84,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
public async Task ReadAll_NotMember_NoSuccess(
|
||||
CurrentContextOrganization organization,
|
||||
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(
|
||||
new[] { OrganizationUserUserDetailsOperations.ReadAll },
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -15,7 +18,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
public class VerifyOrganizationDomainCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -37,7 +40,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -61,7 +64,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -120,7 +123,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var domain = new OrganizationDomain()
|
||||
{
|
||||
@ -137,4 +140,97 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -76,48 +76,4 @@ public class OrganizationDomainServiceTests
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(1)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
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.Repositories;
|
||||
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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -237,6 +237,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
MasterPasswordHash = DefaultPassword
|
||||
});
|
||||
var userManager = factory.GetService<UserManager<User>>();
|
||||
await factory.RegisterAsync(new RegisterRequestModel
|
||||
{
|
||||
Email = DefaultUsername,
|
||||
MasterPasswordHash = DefaultPassword
|
||||
});
|
||||
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
||||
Assert.NotNull(user);
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
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.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -11,8 +10,8 @@ using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Identity.Test.Wrappers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
@ -32,18 +31,14 @@ public class BaseRequestValidatorTests
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<BaseRequestValidatorTests> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
@ -52,43 +47,35 @@ public class BaseRequestValidatorTests
|
||||
|
||||
public BaseRequestValidatorTests()
|
||||
{
|
||||
_userManager = SubstituteUserManager();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_eventService = Substitute.For<IEventService>();
|
||||
_deviceValidator = Substitute.For<IDeviceValidator>();
|
||||
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
|
||||
_duoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_applicationCacheService = Substitute.For<IApplicationCacheService>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_globalSettings = Substitute.For<GlobalSettings>();
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_policyService = Substitute.For<IPolicyService>();
|
||||
_tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_userManager = SubstituteUserManager();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
_userService,
|
||||
_eventService,
|
||||
_deviceValidator,
|
||||
_organizationDuoWebTokenProvider,
|
||||
_duoWebV4SDKService,
|
||||
_organizationRepository,
|
||||
_twoFactorAuthenticationValidator,
|
||||
_organizationUserRepository,
|
||||
_applicationCacheService,
|
||||
_mailService,
|
||||
_logger,
|
||||
_currentContext,
|
||||
_globalSettings,
|
||||
_userRepository,
|
||||
_policyService,
|
||||
_tokenDataFactory,
|
||||
_featureService,
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder);
|
||||
@ -202,6 +189,9 @@ public class BaseRequestValidatorTests
|
||||
{
|
||||
// Arrange
|
||||
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;
|
||||
_sut.isValid = true;
|
||||
@ -230,6 +220,9 @@ public class BaseRequestValidatorTests
|
||||
{
|
||||
// Arrange
|
||||
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;
|
||||
_sut.isValid = true;
|
||||
@ -271,6 +264,9 @@ public class BaseRequestValidatorTests
|
||||
|
||||
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(device);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
@ -306,6 +302,9 @@ public class BaseRequestValidatorTests
|
||||
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(Task.FromResult(true));
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
@ -330,6 +329,9 @@ public class BaseRequestValidatorTests
|
||||
context.ValidatedTokenRequest.ClientId = "Not Web";
|
||||
_sut.isValid = 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
|
||||
await _sut.ValidateAsync(context);
|
||||
@ -341,28 +343,6 @@ public class BaseRequestValidatorTests
|
||||
, 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(
|
||||
ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
|
@ -4,7 +4,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
|
@ -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\"}}}",
|
||||
_ => "{}",
|
||||
};
|
||||
}
|
||||
}
|
@ -1,16 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
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.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -54,38 +51,30 @@ IBaseRequestValidatorTestWrapper
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
|
||||
base(
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
organizationDuoWebTokenProvider,
|
||||
duoWebV4SDKService,
|
||||
organizationRepository,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
applicationCacheService,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
tokenDataFactory,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
@ -98,13 +87,6 @@ IBaseRequestValidatorTestWrapper
|
||||
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(
|
||||
BaseRequestValidationContextFake context)
|
||||
{
|
||||
|
96
test/Identity.Test/Wrappers/UserManagerTestWrapper.cs
Normal file
96
test/Identity.Test/Wrappers/UserManagerTestWrapper.cs
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user