1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-02 04:11:04 +01:00

[PM-6666] Two factor Validator refactor (#4894)

* initial device removal

* Unit Testing

* Finalized tests

* initial commit refactoring two factor

* initial tests

* Unit Tests

* initial device removal

* Unit Testing

* Finalized tests

* initial commit refactoring two factor

* initial tests

* Unit Tests

* Fixing some tests

* renaming and reorganizing

* refactored two factor flows

* fixed a possible issue with object mapping.

* Update TwoFactorAuthenticationValidator.cs

removed unused code
This commit is contained in:
Ike 2024-10-24 10:41:25 -07:00 committed by GitHub
parent 0c346d6070
commit c028c68d9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1119 additions and 380 deletions

View File

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

View File

@ -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
{
@ -238,67 +235,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)
@ -329,35 +265,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")
@ -367,7 +281,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)
@ -379,134 +292,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
@ -546,7 +331,7 @@ public abstract class BaseRequestValidator<T> where T : class
}
/// <summary>
/// checks to see if a user is trying to log into a new device
/// checks to see if a user is trying to log into a new device
/// and has reached the maximum number of failed login attempts.
/// </summary>
/// <param name="unknownDevice">boolean</param>

View File

@ -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")

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,297 @@

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

View File

@ -1,10 +1,8 @@
using System.Security.Claims;
using System.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)
{

View File

@ -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

View File

@ -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);

View File

@ -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);
@ -116,7 +103,7 @@ public class BaseRequestValidatorTests
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
// Assert
// Assert
await _eventService.Received(1)
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id,
Core.Enums.EventType.User_FailedLogIn);
@ -127,7 +114,7 @@ public class BaseRequestValidatorTests
/* Logic path
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
(self hosted) |-> _logger.LogWarning()
(self hosted) |-> _logger.LogWarning()
|-> SetErrorResult
*/
[Theory, BitAutoData]
@ -154,7 +141,7 @@ public class BaseRequestValidatorTests
/* Logic path
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|-> SetErrorResult
*/
[Theory, BitAutoData]
@ -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;
@ -237,7 +230,7 @@ public class BaseRequestValidatorTests
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device);
@ -267,10 +260,13 @@ public class BaseRequestValidatorTests
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
_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,10 +302,13 @@ 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);
// Assert
// Assert
Assert.True(context.GrantResult.IsError);
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
Assert.Equal("SSO authentication is required.", errorResponse.Message);
@ -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,

View File

@ -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;

View File

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

View File

@ -1,16 +1,13 @@
using System.Security.Claims;
using 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)
{

View File

@ -0,0 +1,96 @@

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