mirror of
https://github.com/bitwarden/server.git
synced 2024-11-23 12:25:16 +01:00
[PM-8107] Remove Duo v2 from server (#4934)
refactor(TwoFactorAuthentication): Remove references to old Duo SDK version 2 code and replace them with the Duo SDK version 4 supported library DuoUniversal code. Increased unit test coverage in the Two Factor Authentication code space. We opted to use DI instead of Inheritance for the Duo and OrganizaitonDuo two factor tokens to increase testability, since creating a testing mock of the Duo.Client was non-trivial. Reviewed-by: @JaredSnider-Bitwarden
This commit is contained in:
parent
e16cad50b1
commit
ab5d4738d6
@ -4,15 +4,14 @@ using Bit.Api.Auth.Models.Response.TwoFactor;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
@ -29,11 +28,10 @@ public class TwoFactorController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
||||
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
||||
|
||||
@ -41,22 +39,20 @@ public class TwoFactorController : Controller
|
||||
IUserService userService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
GlobalSettings globalSettings,
|
||||
UserManager<User> userManager,
|
||||
ICurrentContext currentContext,
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||
IFeatureService featureService,
|
||||
IDuoUniversalTokenService duoUniversalConfigService,
|
||||
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_globalSettings = globalSettings;
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||
_featureService = featureService;
|
||||
_duoUniversalTokenService = duoUniversalConfigService;
|
||||
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
||||
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
||||
}
|
||||
@ -184,21 +180,7 @@ public class TwoFactorController : Controller
|
||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
try
|
||||
{
|
||||
// for backwards compatibility - will be removed with PM-8107
|
||||
DuoApi duoApi = null;
|
||||
if (model.ClientId != null && model.ClientSecret != null)
|
||||
{
|
||||
duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host);
|
||||
}
|
||||
else
|
||||
{
|
||||
duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
||||
}
|
||||
await duoApi.JSONApiCall("GET", "/auth/v2/check");
|
||||
}
|
||||
catch (DuoException)
|
||||
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
@ -241,21 +223,7 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();
|
||||
try
|
||||
{
|
||||
// for backwards compatibility - will be removed with PM-8107
|
||||
DuoApi duoApi = null;
|
||||
if (model.ClientId != null && model.ClientSecret != null)
|
||||
{
|
||||
duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host);
|
||||
}
|
||||
else
|
||||
{
|
||||
duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
||||
}
|
||||
await duoApi.JSONApiCall("GET", "/auth/v2/check");
|
||||
}
|
||||
catch (DuoException)
|
||||
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
|
@ -2,8 +2,8 @@
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
@ -43,21 +43,16 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
|
||||
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
|
||||
{
|
||||
/*
|
||||
To support both v2 and v4 we need to remove the required annotation from the properties.
|
||||
todo - the required annotation will be added back in PM-8107.
|
||||
String lengths based on Duo's documentation
|
||||
https://github.com/duosecurity/duo_universal_csharp/blob/main/DuoUniversal/Client.cs
|
||||
*/
|
||||
[StringLength(50)]
|
||||
public string ClientId { get; set; }
|
||||
[StringLength(50)]
|
||||
public string ClientSecret { get; set; }
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
[StringLength(50)]
|
||||
public string IntegrationKey { get; set; }
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
[StringLength(50)]
|
||||
public string SecretKey { get; set; }
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(20, MinimumLength = 20, ErrorMessage = "Client Id must be exactly 20 characters.")]
|
||||
public string ClientId { get; set; }
|
||||
[Required]
|
||||
[StringLength(40, MinimumLength = 40, ErrorMessage = "Client Secret must be exactly 40 characters.")]
|
||||
public string ClientSecret { get; set; }
|
||||
[Required]
|
||||
public string Host { get; set; }
|
||||
|
||||
public User ToUser(User existingUser)
|
||||
@ -65,22 +60,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
var providers = existingUser.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
{
|
||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
providers = [];
|
||||
}
|
||||
else if (providers.ContainsKey(TwoFactorProviderType.Duo))
|
||||
{
|
||||
providers.Remove(TwoFactorProviderType.Duo);
|
||||
}
|
||||
|
||||
Temporary_SyncDuoParams();
|
||||
|
||||
providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object>
|
||||
{
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
["SKey"] = SecretKey,
|
||||
["IKey"] = IntegrationKey,
|
||||
["ClientSecret"] = ClientSecret,
|
||||
["ClientId"] = ClientId,
|
||||
["Host"] = Host
|
||||
@ -96,22 +86,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
var providers = existingOrg.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
{
|
||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
providers = [];
|
||||
}
|
||||
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo))
|
||||
{
|
||||
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
||||
}
|
||||
|
||||
Temporary_SyncDuoParams();
|
||||
|
||||
providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object>
|
||||
{
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
["SKey"] = SecretKey,
|
||||
["IKey"] = IntegrationKey,
|
||||
["ClientSecret"] = ClientSecret,
|
||||
["ClientId"] = ClientId,
|
||||
["Host"] = Host
|
||||
@ -124,34 +109,22 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (!DuoApi.ValidHost(Host))
|
||||
var results = new List<ValidationResult>();
|
||||
if (string.IsNullOrWhiteSpace(ClientId))
|
||||
{
|
||||
yield return new ValidationResult("Host is invalid.", [nameof(Host)]);
|
||||
results.Add(new ValidationResult("ClientId is required.", [nameof(ClientId)]));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId) &&
|
||||
string.IsNullOrWhiteSpace(SecretKey) && string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
{
|
||||
yield return new ValidationResult("Neither v2 or v4 values are valid.", [nameof(IntegrationKey), nameof(SecretKey), nameof(ClientSecret), nameof(ClientId)]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
use this method to ensure that both v2 params and v4 params are in sync
|
||||
todo will be removed in pm-8107
|
||||
*/
|
||||
private void Temporary_SyncDuoParams()
|
||||
{
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId))
|
||||
if (string.IsNullOrWhiteSpace(ClientSecret))
|
||||
{
|
||||
SecretKey = ClientSecret;
|
||||
IntegrationKey = ClientId;
|
||||
results.Add(new ValidationResult("ClientSecret is required.", [nameof(ClientSecret)]));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Host) || !DuoUniversalTokenService.ValidDuoHost(Host))
|
||||
{
|
||||
ClientSecret = SecretKey;
|
||||
ClientId = IntegrationKey;
|
||||
results.Add(new ValidationResult("Host is invalid.", [nameof(Host)]));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,37 +13,26 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
public TwoFactorDuoResponseModel(User user)
|
||||
: base(ResponseObj)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
Build(provider);
|
||||
}
|
||||
|
||||
public TwoFactorDuoResponseModel(Organization org)
|
||||
public TwoFactorDuoResponseModel(Organization organization)
|
||||
: base(ResponseObj)
|
||||
{
|
||||
if (org == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(org));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(organization);
|
||||
|
||||
var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
Build(provider);
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
public string Host { get; set; }
|
||||
//TODO - will remove SecretKey with PM-8107
|
||||
public string SecretKey { get; set; }
|
||||
//TODO - will remove IntegrationKey with PM-8107
|
||||
public string IntegrationKey { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
|
||||
// updated build to assist in the EDD migration for the Duo 2FA provider
|
||||
private void Build(TwoFactorProvider provider)
|
||||
{
|
||||
if (provider?.MetaData != null && provider.MetaData.Count > 0)
|
||||
@ -54,36 +43,13 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
{
|
||||
Host = (string)host;
|
||||
}
|
||||
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
// check Skey and IKey first if they exist
|
||||
if (provider.MetaData.TryGetValue("SKey", out var sKey))
|
||||
{
|
||||
ClientSecret = MaskKey((string)sKey);
|
||||
SecretKey = MaskKey((string)sKey);
|
||||
}
|
||||
if (provider.MetaData.TryGetValue("IKey", out var iKey))
|
||||
{
|
||||
IntegrationKey = (string)iKey;
|
||||
ClientId = (string)iKey;
|
||||
}
|
||||
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace((string)clientSecret))
|
||||
{
|
||||
ClientSecret = MaskKey((string)clientSecret);
|
||||
SecretKey = MaskKey((string)clientSecret);
|
||||
}
|
||||
ClientSecret = MaskSecret((string)clientSecret);
|
||||
}
|
||||
if (provider.MetaData.TryGetValue("ClientId", out var clientId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace((string)clientId))
|
||||
{
|
||||
ClientId = (string)clientId;
|
||||
IntegrationKey = (string)clientId;
|
||||
}
|
||||
ClientId = (string)clientId;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -92,30 +58,7 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
use this method to ensure that both v2 params and v4 params are in sync
|
||||
todo will be removed in pm-8107
|
||||
*/
|
||||
private void Temporary_SyncDuoParams()
|
||||
{
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId))
|
||||
{
|
||||
SecretKey = ClientSecret;
|
||||
IntegrationKey = ClientId;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
{
|
||||
ClientSecret = SecretKey;
|
||||
ClientId = IntegrationKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidDataException("Invalid Duo parameters.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string MaskKey(string key)
|
||||
private static string MaskSecret(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || key.Length <= 6)
|
||||
{
|
||||
|
@ -23,7 +23,6 @@ using Microsoft.OpenApi.Models;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
@ -32,10 +31,10 @@ using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.SecretsManager;
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
|
@ -1,86 +0,0 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities.Duo;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
public class DuoWebTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public DuoWebTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var signatureRequest = DuoWeb.SignRequest((string)provider.MetaData["IKey"],
|
||||
(string)provider.MetaData["SKey"], _globalSettings.Duo.AKey, user.Email);
|
||||
return signatureRequest;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = DuoWeb.VerifyResponse((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"],
|
||||
_globalSettings.Duo.AKey, token);
|
||||
|
||||
return response == user.Email;
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
|
||||
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities.Duo;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
public interface IOrganizationDuoWebTokenProvider : IOrganizationTwoFactorTokenProvider { }
|
||||
|
||||
public class OrganizationDuoWebTokenProvider : IOrganizationDuoWebTokenProvider
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public OrganizationDuoWebTokenProvider(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo)
|
||||
&& HasProperMetaData(provider);
|
||||
return Task.FromResult(canGenerate);
|
||||
}
|
||||
|
||||
public Task<string> GenerateAsync(Organization organization, User user)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
var signatureRequest = DuoWeb.SignRequest(provider.MetaData["IKey"].ToString(),
|
||||
provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, user.Email);
|
||||
return Task.FromResult(signatureRequest);
|
||||
}
|
||||
|
||||
public Task<bool> ValidateAsync(string token, Organization organization, User user)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var response = DuoWeb.VerifyResponse(provider.MetaData["IKey"].ToString(),
|
||||
provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, token);
|
||||
|
||||
return Task.FromResult(response == user.Email);
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
|
||||
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
/*
|
||||
PM-5156 addresses tech debt
|
||||
Interface to allow for DI, will end up being removed as part of the removal of the old Duo SDK v2 flows.
|
||||
This service is to support SDK v4 flows for Duo. At some time in the future we will need
|
||||
to combine this service with the DuoWebTokenProvider and OrganizationDuoWebTokenProvider to support SDK v4.
|
||||
*/
|
||||
public interface ITemporaryDuoWebV4SDKService
|
||||
{
|
||||
Task<string> GenerateAsync(TwoFactorProvider provider, User user);
|
||||
Task<bool> ValidateAsync(string token, TwoFactorProvider provider, User user);
|
||||
}
|
||||
|
||||
public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
|
||||
private readonly ILogger<TemporaryDuoWebV4SDKService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
|
||||
/// </summary>
|
||||
/// <param name="currentContext">used to fetch initiating Client</param>
|
||||
/// <param name="globalSettings">used to fetch vault URL for Redirect URL</param>
|
||||
public TemporaryDuoWebV4SDKService(
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
ILogger<TemporaryDuoWebV4SDKService> logger)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider agnostic (either Duo or OrganizationDuo) method to generate a Duo Auth URL
|
||||
/// </summary>
|
||||
/// <param name="provider">Either Duo or OrganizationDuo</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <returns>AuthUrl for DUO SDK v4</returns>
|
||||
public async Task<string> GenerateAsync(TwoFactorProvider provider, User user)
|
||||
{
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
if (!HasProperMetaData_SDKV2(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var duoClient = await BuildDuoClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var state = _tokenDataFactory.Protect(new DuoUserStateTokenable(user));
|
||||
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Duo SDK v4 response
|
||||
/// </summary>
|
||||
/// <param name="token">response form Duo</param>
|
||||
/// <param name="provider">TwoFactorProviderType Duo or OrganizationDuo</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <returns>true or false depending on result of verification</returns>
|
||||
public async Task<bool> ValidateAsync(string token, TwoFactorProvider provider, User user)
|
||||
{
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
if (!HasProperMetaData_SDKV2(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var duoClient = await BuildDuoClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = token.Split("|");
|
||||
var authCode = parts[0];
|
||||
var state = parts[1];
|
||||
|
||||
_tokenDataFactory.TryUnprotect(state, out var tokenable);
|
||||
if (!tokenable.Valid || !tokenable.TokenIsValid(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used
|
||||
// their authCode with a victims credentials
|
||||
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);
|
||||
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
|
||||
return res.AuthResult.Result == "allow";
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") &&
|
||||
provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the metadata for SDK V2 is present.
|
||||
/// Transitional method to support Duo during v4 database rename
|
||||
/// </summary>
|
||||
/// <param name="provider">The TwoFactorProvider object to check.</param>
|
||||
/// <returns>True if the provider has the proper metadata; otherwise, false.</returns>
|
||||
private bool HasProperMetaData_SDKV2(TwoFactorProvider provider)
|
||||
{
|
||||
if (provider?.MetaData != null &&
|
||||
provider.MetaData.TryGetValue("IKey", out var iKey) &&
|
||||
provider.MetaData.TryGetValue("SKey", out var sKey) &&
|
||||
provider.MetaData.ContainsKey("Host"))
|
||||
{
|
||||
provider.MetaData.Add("ClientId", iKey);
|
||||
provider.MetaData.Add("ClientSecret", sKey);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation
|
||||
/// </summary>
|
||||
/// <param name="provider">TwoFactorProvider Duo or OrganizationDuo</param>
|
||||
/// <returns>Duo.Client object or null</returns>
|
||||
private async Task<Duo.Client> BuildDuoClientAsync(TwoFactorProvider provider)
|
||||
{
|
||||
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
|
||||
// to redirect back to the initiating client
|
||||
_currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName);
|
||||
var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}",
|
||||
_globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web");
|
||||
|
||||
var client = new Duo.ClientBuilder(
|
||||
(string)provider.MetaData["ClientId"],
|
||||
(string)provider.MetaData["ClientSecret"],
|
||||
(string)provider.MetaData["Host"],
|
||||
redirectUri).Build();
|
||||
|
||||
if (!await client.DoHealthCheck(true))
|
||||
{
|
||||
_logger.LogError("Unable to connect to Duo. Health check failed.");
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtpNet;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -0,0 +1,102 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class DuoUniversalTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
/// <summary>
|
||||
/// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance
|
||||
/// occurring between IUserService, which extends the UserManager<User>, and the usage of the
|
||||
/// UserManager<User> within this class. Trying to resolve the IUserService using the DI pipeline
|
||||
/// will not allow the server to start and it will hang and give no helpful indication as to the problem.
|
||||
/// </summary>
|
||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (provider == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(user);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(user);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Duo Two Factor Provider for the user if they have access to Duo
|
||||
/// </summary>
|
||||
/// <param name="user">Active User</param>
|
||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||
private async Task<TwoFactorProvider> GetDuoTwoFactorProvider(User user, IUserService userService)
|
||||
{
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the User to fetch a valid TwoFactorProvider and use it to create a Duo.Client
|
||||
/// </summary>
|
||||
/// <param name="user">active user</param>
|
||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||
private async Task<Duo.Client> GetDuoClientAsync(User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (provider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return duoClient;
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
/// <summary>
|
||||
/// OrganizationDuo and Duo TwoFactorProviderTypes both use the same flows so both of those Token Providers will
|
||||
/// have this class injected to utilize these methods
|
||||
/// </summary>
|
||||
public interface IDuoUniversalTokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the Duo Auth URL for the user to be redirected to Duo for 2FA. This
|
||||
/// Auth URL also lets the Duo Service know where to redirect the user back to after
|
||||
/// the 2FA process is complete.
|
||||
/// </summary>
|
||||
/// <param name="duoClient">A not null valid Duo.Client</param>
|
||||
/// <param name="tokenDataFactory">This service creates the state token for added security</param>
|
||||
/// <param name="user">currently active user</param>
|
||||
/// <returns>a URL in string format</returns>
|
||||
string GenerateAuthUrl(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user);
|
||||
|
||||
/// <summary>
|
||||
/// Makes the request to Duo to validate the authCode and state token
|
||||
/// </summary>
|
||||
/// <param name="duoClient">A not null valid Duo.Client</param>
|
||||
/// <param name="tokenDataFactory">Factory for decrypting the state</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <param name="token">token received from the client</param>
|
||||
/// <returns>boolean based on result from Duo</returns>
|
||||
Task<bool> RequestDuoValidationAsync(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user,
|
||||
string token);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This method is to validate a Duo configuration
|
||||
/// when adding or updating the configuration. This method makes a web request to Duo to verify the configuration.
|
||||
/// Throws exception if configuration is invalid.
|
||||
/// </summary>
|
||||
/// <param name="clientSecret">Duo client Secret</param>
|
||||
/// <param name="clientId">Duo client Id</param>
|
||||
/// <param name="host">Duo host</param>
|
||||
/// <returns>Boolean</returns>
|
||||
Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host);
|
||||
|
||||
/// <summary>
|
||||
/// Checks provider for the correct Duo metadata: ClientId, ClientSecret, and Host. Does no validation on the data.
|
||||
/// it is assumed to be correct. The only way to have the data written to the Database is after verification
|
||||
/// occurs.
|
||||
/// </summary>
|
||||
/// <param name="provider">Host being checked for proper data</param>
|
||||
/// <returns>true if all three are present; false if one is missing or the host is incorrect</returns>
|
||||
bool HasProperDuoMetadata(TwoFactorProvider provider);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation.
|
||||
/// This method is made public so that it is easier to test. If the method was private then there would not be an
|
||||
/// easy way to mock the response. Since this makes a web request it is difficult to mock.
|
||||
/// </summary>
|
||||
/// <param name="provider">TwoFactorProvider Duo or OrganizationDuo</param>
|
||||
/// <returns>Duo.Client object or null</returns>
|
||||
Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider);
|
||||
}
|
||||
|
||||
public class DuoUniversalTokenService(
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings) : IDuoUniversalTokenService
|
||||
{
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
private readonly GlobalSettings _globalSettings = globalSettings;
|
||||
|
||||
public string GenerateAuthUrl(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user)
|
||||
{
|
||||
var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user));
|
||||
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
public async Task<bool> RequestDuoValidationAsync(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
var parts = token.Split("|");
|
||||
var authCode = parts[0];
|
||||
var state = parts[1];
|
||||
tokenDataFactory.TryUnprotect(state, out var tokenable);
|
||||
if (!tokenable.Valid || !tokenable.TokenIsValid(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used
|
||||
// their authCode with a victims credentials
|
||||
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);
|
||||
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
|
||||
return res.AuthResult.Result == "allow";
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host)
|
||||
{
|
||||
// Do some simple checks to ensure data integrity
|
||||
if (!ValidDuoHost(host) ||
|
||||
string.IsNullOrWhiteSpace(clientSecret) ||
|
||||
string.IsNullOrWhiteSpace(clientId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The AuthURI is not important for this health check so we pass in a non-empty string
|
||||
var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build();
|
||||
|
||||
// This could throw an exception, the false flag will allow the exception to bubble up
|
||||
return await client.DoHealthCheck(false);
|
||||
}
|
||||
|
||||
public bool HasProperDuoMetadata(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null &&
|
||||
provider.MetaData.ContainsKey("ClientId") &&
|
||||
provider.MetaData.ContainsKey("ClientSecret") &&
|
||||
provider.MetaData.ContainsKey("Host") &&
|
||||
ValidDuoHost((string)provider.MetaData["Host"]);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks the host string to make sure it meets Duo's Guidelines before attempting to create a Duo.Client.
|
||||
/// </summary>
|
||||
/// <param name="host">string representing the Duo Host</param>
|
||||
/// <returns>true if the host is valid false otherwise</returns>
|
||||
public static bool ValidDuoHost(string host)
|
||||
{
|
||||
if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri))
|
||||
{
|
||||
return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") &&
|
||||
uri.Host.StartsWith("api-") &&
|
||||
(uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider)
|
||||
{
|
||||
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
|
||||
// to redirect back to the initiating client
|
||||
_currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName);
|
||||
var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}",
|
||||
_globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web");
|
||||
|
||||
var client = new Duo.ClientBuilder(
|
||||
(string)provider.MetaData["ClientId"],
|
||||
(string)provider.MetaData["ClientSecret"],
|
||||
(string)provider.MetaData["Host"],
|
||||
redirectUri).Build();
|
||||
|
||||
if (!await client.DoHealthCheck(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public interface IOrganizationTwoFactorTokenProvider
|
||||
{
|
@ -0,0 +1,81 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { }
|
||||
|
||||
public class OrganizationDuoUniversalTokenProvider(
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
IDuoUniversalTokenService duoUniversalTokenService) : IOrganizationDuoUniversalTokenProvider
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;
|
||||
|
||||
public Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization)
|
||||
{
|
||||
var provider = GetDuoTwoFactorProvider(organization);
|
||||
if (provider != null && provider.Enabled)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(Organization organization, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(organization);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string token, Organization organization, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(organization);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);
|
||||
}
|
||||
|
||||
private TwoFactorProvider GetDuoTwoFactorProvider(Organization organization)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private async Task<Duo.Client> GetDuoClientAsync(Organization organization)
|
||||
{
|
||||
var provider = GetDuoTwoFactorProvider(organization);
|
||||
if (provider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return duoClient;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider<User>
|
||||
{
|
@ -10,7 +10,7 @@ using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YubicoDotNetClient;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
@ -24,7 +24,7 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -46,7 +46,7 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return false;
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
/*
|
||||
Original source modified from https://github.com/duosecurity/duo_api_csharp
|
||||
|
||||
=============================================================================
|
||||
=============================================================================
|
||||
|
||||
Copyright (c) 2018 Duo Security
|
||||
All rights reserved
|
||||
*/
|
||||
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Bit.Core.Models.Api.Response.Duo;
|
||||
|
||||
namespace Bit.Core.Auth.Utilities;
|
||||
|
||||
public class DuoApi
|
||||
{
|
||||
private const string UrlScheme = "https";
|
||||
private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)";
|
||||
|
||||
private readonly string _host;
|
||||
private readonly string _ikey;
|
||||
private readonly string _skey;
|
||||
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
public DuoApi(string ikey, string skey, string host)
|
||||
{
|
||||
_ikey = ikey;
|
||||
_skey = skey;
|
||||
_host = host;
|
||||
|
||||
if (!ValidHost(host))
|
||||
{
|
||||
throw new DuoException("Invalid Duo host configured.", new ArgumentException(nameof(host)));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ValidHost(string host)
|
||||
{
|
||||
if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri))
|
||||
{
|
||||
return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") &&
|
||||
uri.Host.StartsWith("api-") &&
|
||||
(uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string CanonicalizeParams(Dictionary<string, string> parameters)
|
||||
{
|
||||
var ret = new List<string>();
|
||||
foreach (var pair in parameters)
|
||||
{
|
||||
var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value));
|
||||
// Signatures require upper-case hex digits.
|
||||
p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant());
|
||||
// Escape only the expected characters.
|
||||
p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X"));
|
||||
p = p.Replace("%7E", "~");
|
||||
// UrlEncode converts space (" ") to "+". The
|
||||
// signature algorithm requires "%20" instead. Actual
|
||||
// + has already been replaced with %2B.
|
||||
p = p.Replace("+", "%20");
|
||||
ret.Add(p);
|
||||
}
|
||||
|
||||
ret.Sort(StringComparer.Ordinal);
|
||||
return string.Join("&", ret.ToArray());
|
||||
}
|
||||
|
||||
protected string CanonicalizeRequest(string method, string path, string canonParams, string date)
|
||||
{
|
||||
string[] lines = {
|
||||
date,
|
||||
method.ToUpperInvariant(),
|
||||
_host.ToLower(),
|
||||
path,
|
||||
canonParams,
|
||||
};
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
public string Sign(string method, string path, string canonParams, string date)
|
||||
{
|
||||
var canon = CanonicalizeRequest(method, path, canonParams, date);
|
||||
var sig = HmacSign(canon);
|
||||
var auth = string.Concat(_ikey, ':', sig);
|
||||
return string.Concat("Basic ", Encode64(auth));
|
||||
}
|
||||
|
||||
/// <param name="timeout">The request timeout, in milliseconds.
|
||||
/// Specify 0 to use the system-default timeout. Use caution if
|
||||
/// you choose to specify a custom timeout - some API
|
||||
/// calls (particularly in the Auth APIs) will not
|
||||
/// return a response until an out-of-band authentication process
|
||||
/// has completed. In some cases, this may take as much as a
|
||||
/// small number of minutes.</param>
|
||||
private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary<string, string> parameters, int timeout)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
parameters = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var canonParams = CanonicalizeParams(parameters);
|
||||
var query = string.Empty;
|
||||
if (!method.Equals("POST") && !method.Equals("PUT"))
|
||||
{
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
query = "?" + canonParams;
|
||||
}
|
||||
}
|
||||
var url = $"{UrlScheme}://{_host}{path}{query}";
|
||||
|
||||
var dateString = RFC822UtcNow();
|
||||
var auth = Sign(method, path, canonParams, dateString);
|
||||
|
||||
var request = new HttpRequestMessage
|
||||
{
|
||||
Method = new HttpMethod(method),
|
||||
RequestUri = new Uri(url),
|
||||
};
|
||||
request.Headers.Add("Authorization", auth);
|
||||
request.Headers.Add("X-Duo-Date", dateString);
|
||||
request.Headers.UserAgent.ParseAdd(UserAgent);
|
||||
|
||||
if (timeout > 0)
|
||||
{
|
||||
_httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);
|
||||
}
|
||||
|
||||
if (method.Equals("POST") || method.Equals("PUT"))
|
||||
{
|
||||
request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
var statusCode = response.StatusCode;
|
||||
return (result, statusCode);
|
||||
}
|
||||
|
||||
public async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> parameters = null)
|
||||
{
|
||||
return await JSONApiCall(method, path, parameters, 0);
|
||||
}
|
||||
|
||||
/// <param name="timeout">The request timeout, in milliseconds.
|
||||
/// Specify 0 to use the system-default timeout. Use caution if
|
||||
/// you choose to specify a custom timeout - some API
|
||||
/// calls (particularly in the Auth APIs) will not
|
||||
/// return a response until an out-of-band authentication process
|
||||
/// has completed. In some cases, this may take as much as a
|
||||
/// small number of minutes.</param>
|
||||
private async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> parameters, int timeout)
|
||||
{
|
||||
var (res, statusCode) = await ApiCall(method, path, parameters, timeout);
|
||||
try
|
||||
{
|
||||
var obj = JsonSerializer.Deserialize<DuoResponseModel>(res);
|
||||
if (obj.Stat == "OK")
|
||||
{
|
||||
return obj.Response;
|
||||
}
|
||||
|
||||
throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail);
|
||||
}
|
||||
catch (ApiException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new BadResponseException((int)statusCode, e);
|
||||
}
|
||||
}
|
||||
|
||||
private int? ToNullableInt(string s)
|
||||
{
|
||||
int i;
|
||||
if (int.TryParse(s, out i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string HmacSign(string data)
|
||||
{
|
||||
var keyBytes = Encoding.ASCII.GetBytes(_skey);
|
||||
var dataBytes = Encoding.ASCII.GetBytes(data);
|
||||
|
||||
using (var hmac = new HMACSHA1(keyBytes))
|
||||
{
|
||||
var hash = hmac.ComputeHash(dataBytes);
|
||||
var hex = BitConverter.ToString(hash);
|
||||
return hex.Replace("-", string.Empty).ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Encode64(string plaintext)
|
||||
{
|
||||
var plaintextBytes = Encoding.ASCII.GetBytes(plaintext);
|
||||
return Convert.ToBase64String(plaintextBytes);
|
||||
}
|
||||
|
||||
private static string RFC822UtcNow()
|
||||
{
|
||||
// Can't use the "zzzz" format because it adds a ":"
|
||||
// between the offset's hours and minutes.
|
||||
var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
var offset = 0;
|
||||
var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
|
||||
dateString += " " + zone.PadRight(5, '0');
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
public class DuoException : Exception
|
||||
{
|
||||
public int HttpStatus { get; private set; }
|
||||
|
||||
public DuoException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{ }
|
||||
|
||||
public DuoException(int httpStatus, string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
HttpStatus = httpStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiException : DuoException
|
||||
{
|
||||
public int Code { get; private set; }
|
||||
public string ApiMessage { get; private set; }
|
||||
public string ApiMessageDetail { get; private set; }
|
||||
|
||||
public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail)
|
||||
: base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null)
|
||||
{
|
||||
Code = code;
|
||||
ApiMessage = apiMessage;
|
||||
ApiMessageDetail = apiMessageDetail;
|
||||
}
|
||||
|
||||
private static string FormatMessage(int code, string apiMessage, string apiMessageDetail)
|
||||
{
|
||||
return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail);
|
||||
}
|
||||
}
|
||||
|
||||
public class BadResponseException : DuoException
|
||||
{
|
||||
public BadResponseException(int httpStatus, Exception inner)
|
||||
: base(httpStatus, FormatMessage(httpStatus, inner), inner)
|
||||
{ }
|
||||
|
||||
private static string FormatMessage(int httpStatus, Exception inner)
|
||||
{
|
||||
var innerMessage = "(null)";
|
||||
if (inner != null)
|
||||
{
|
||||
innerMessage = string.Format("'{0}'", inner.Message);
|
||||
}
|
||||
return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus);
|
||||
}
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
/*
|
||||
Original source modified from https://github.com/duosecurity/duo_dotnet
|
||||
|
||||
=============================================================================
|
||||
=============================================================================
|
||||
|
||||
ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2011, Duo Security, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Auth.Utilities.Duo;
|
||||
|
||||
public static class DuoWeb
|
||||
{
|
||||
private const string DuoProfix = "TX";
|
||||
private const string AppPrefix = "APP";
|
||||
private const string AuthPrefix = "AUTH";
|
||||
private const int DuoExpire = 300;
|
||||
private const int AppExpire = 3600;
|
||||
private const int IKeyLength = 20;
|
||||
private const int SKeyLength = 40;
|
||||
private const int AKeyLength = 40;
|
||||
|
||||
public static string ErrorUser = "ERR|The username passed to sign_request() is invalid.";
|
||||
public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid.";
|
||||
public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid.";
|
||||
public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " +
|
||||
"40 characters.";
|
||||
public static string ErrorUnknown = "ERR|An unknown error has occurred.";
|
||||
|
||||
// throw on invalid bytes
|
||||
private static Encoding _encoding = new UTF8Encoding(false, true);
|
||||
private static DateTime _epoc = new DateTime(1970, 1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a signed request for Duo authentication.
|
||||
/// The returned value should be passed into the Duo.init() call
|
||||
/// in the rendered web page used for Duo authentication.
|
||||
/// </summary>
|
||||
/// <param name="ikey">Duo integration key</param>
|
||||
/// <param name="skey">Duo secret key</param>
|
||||
/// <param name="akey">Application secret key</param>
|
||||
/// <param name="username">Primary-authenticated username</param>
|
||||
/// <param name="currentTime">(optional) The current UTC time</param>
|
||||
/// <returns>signed request</returns>
|
||||
public static string SignRequest(string ikey, string skey, string akey, string username,
|
||||
DateTime? currentTime = null)
|
||||
{
|
||||
string duoSig;
|
||||
string appSig;
|
||||
|
||||
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
||||
|
||||
if (username == string.Empty)
|
||||
{
|
||||
return ErrorUser;
|
||||
}
|
||||
if (username.Contains("|"))
|
||||
{
|
||||
return ErrorUser;
|
||||
}
|
||||
if (ikey.Length != IKeyLength)
|
||||
{
|
||||
return ErrorIKey;
|
||||
}
|
||||
if (skey.Length != SKeyLength)
|
||||
{
|
||||
return ErrorSKey;
|
||||
}
|
||||
if (akey.Length < AKeyLength)
|
||||
{
|
||||
return ErrorAKey;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue);
|
||||
appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ErrorUnknown;
|
||||
}
|
||||
|
||||
return $"{duoSig}:{appSig}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate the signed response returned from Duo.
|
||||
/// Returns the username of the authenticated user, or null.
|
||||
/// </summary>
|
||||
/// <param name="ikey">Duo integration key</param>
|
||||
/// <param name="skey">Duo secret key</param>
|
||||
/// <param name="akey">Application secret key</param>
|
||||
/// <param name="sigResponse">The signed response POST'ed to the server</param>
|
||||
/// <param name="currentTime">(optional) The current UTC time</param>
|
||||
/// <returns>authenticated username, or null</returns>
|
||||
public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse,
|
||||
DateTime? currentTime = null)
|
||||
{
|
||||
string authUser = null;
|
||||
string appUser = null;
|
||||
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
var sigs = sigResponse.Split(':');
|
||||
var authSig = sigs[0];
|
||||
var appSig = sigs[1];
|
||||
|
||||
authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue);
|
||||
appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authUser != appUser)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return authUser;
|
||||
}
|
||||
|
||||
private static string SignVals(string key, string username, string ikey, string prefix, long expire,
|
||||
DateTime currentTime)
|
||||
{
|
||||
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
||||
expire = ts + expire;
|
||||
var val = $"{username}|{ikey}|{expire.ToString()}";
|
||||
var cookie = $"{prefix}|{Encode64(val)}";
|
||||
var sig = Sign(key, cookie);
|
||||
return $"{cookie}|{sig}";
|
||||
}
|
||||
|
||||
private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime)
|
||||
{
|
||||
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
||||
|
||||
var parts = val.Split('|');
|
||||
if (parts.Length != 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var uPrefix = parts[0];
|
||||
var uB64 = parts[1];
|
||||
var uSig = parts[2];
|
||||
|
||||
var sig = Sign(key, $"{uPrefix}|{uB64}");
|
||||
if (Sign(key, sig) != Sign(key, uSig))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uPrefix != prefix)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cookie = Decode64(uB64);
|
||||
var cookieParts = cookie.Split('|');
|
||||
if (cookieParts.Length != 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var username = cookieParts[0];
|
||||
var uIKey = cookieParts[1];
|
||||
var expire = cookieParts[2];
|
||||
|
||||
if (uIKey != ikey)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var expireTs = Convert.ToInt32(expire);
|
||||
if (ts >= expireTs)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
private static string Sign(string skey, string data)
|
||||
{
|
||||
var keyBytes = Encoding.ASCII.GetBytes(skey);
|
||||
var dataBytes = Encoding.ASCII.GetBytes(data);
|
||||
|
||||
using (var hmac = new HMACSHA1(keyBytes))
|
||||
{
|
||||
var hash = hmac.ComputeHash(dataBytes);
|
||||
var hex = BitConverter.ToString(hash);
|
||||
return hex.Replace("-", "").ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Encode64(string plaintext)
|
||||
{
|
||||
var plaintextBytes = _encoding.GetBytes(plaintext);
|
||||
return Convert.ToBase64String(plaintextBytes);
|
||||
}
|
||||
|
||||
private static string Decode64(string encoded)
|
||||
{
|
||||
var plaintextBytes = Convert.FromBase64String(encoded);
|
||||
return _encoding.GetString(plaintextBytes);
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
@ -52,8 +50,7 @@ public interface ITwoFactorAuthenticationValidator
|
||||
public class TwoFactorAuthenticationValidator(
|
||||
IUserService userService,
|
||||
UserManager<User> userManager,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -63,8 +60,7 @@ public class TwoFactorAuthenticationValidator(
|
||||
{
|
||||
private readonly IUserService _userService = userService;
|
||||
private readonly UserManager<User> _userManager = userManager;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService;
|
||||
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;
|
||||
private readonly IFeatureService _featureService = featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
|
||||
@ -153,17 +149,7 @@ public class TwoFactorAuthenticationValidator(
|
||||
{
|
||||
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 await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -181,19 +167,6 @@ public class TwoFactorAuthenticationValidator(
|
||||
{
|
||||
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:
|
||||
@ -248,10 +221,11 @@ public class TwoFactorAuthenticationValidator(
|
||||
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
||||
*/
|
||||
if (type == TwoFactorProviderType.OrganizationDuo &&
|
||||
await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||
await _organizationDuoUniversalTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||
{
|
||||
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
||||
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
|
||||
twoFactorParams.Add("AuthUrl",
|
||||
await _organizationDuoUniversalTokenProvider.GenerateAsync(organization, user));
|
||||
|
||||
return twoFactorParams;
|
||||
}
|
||||
@ -261,13 +235,9 @@ public class TwoFactorAuthenticationValidator(
|
||||
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));
|
||||
twoFactorParams.Add("AuthUrl", token);
|
||||
break;
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
if (token != null)
|
||||
|
@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.LoginFeatures;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -113,6 +114,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<IDeviceService, DeviceService>();
|
||||
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||
services.AddScoped<IAuthRequestService, AuthRequestService>();
|
||||
services.AddScoped<IDuoUniversalTokenService, DuoUniversalTokenService>();
|
||||
services.AddScoped<ISendService, SendService>();
|
||||
services.AddLoginServices();
|
||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||
@ -388,8 +390,7 @@ public static class ServiceCollectionExtensions
|
||||
public static IdentityBuilder AddCustomIdentityServices(
|
||||
this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
services.AddScoped<IOrganizationDuoWebTokenProvider, OrganizationDuoWebTokenProvider>();
|
||||
services.AddScoped<ITemporaryDuoWebV4SDKService, TemporaryDuoWebV4SDKService>();
|
||||
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
||||
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
||||
{
|
||||
@ -430,7 +431,7 @@ public static class ServiceCollectionExtensions
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email))
|
||||
.AddTokenProvider<YubicoOtpTokenProvider>(
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey))
|
||||
.AddTokenProvider<DuoWebTokenProvider>(
|
||||
.AddTokenProvider<DuoUniversalTokenProvider>(
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo))
|
||||
.AddTokenProvider<TwoFactorRememberTokenProvider>(
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))
|
||||
|
295
test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs
Normal file
295
test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs
Normal file
@ -0,0 +1,295 @@
|
||||
using Bit.Api.Auth.Controllers;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Response.TwoFactor;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Auth.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(TwoFactorController))]
|
||||
[SutProviderCustomize]
|
||||
public class TwoFactorControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(null as User);
|
||||
|
||||
// Act
|
||||
var result = () => sutProvider.Sut.GetDuo(request);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CheckAsync_BadSecret_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.VerifySecretAsync(default, default)
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.GetDuo(request);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("The model state is invalid.", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CheckAsync_CannotAccessPremium_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.VerifySecretAsync(default, default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(default)
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.GetDuo(request);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Premium status is required.", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetDuo_Success(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetDuo(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<TwoFactorDuoResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutDuo_InvalidConfiguration_ThrowsBadRequestException(User user, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.ValidateDuoConfiguration(default, default, default)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.PutDuo(request);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutDuo_Success(User user, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.ValidateDuoConfiguration(default, default, default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.PutDuo(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<TwoFactorDuoResponseModel>(result);
|
||||
Assert.Equal(user.TwoFactorProviders, request.ToUser(user).TwoFactorProviders);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CheckOrganizationAsync_ManagePolicies_ThrowsNotFoundException(
|
||||
User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(default)
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException(
|
||||
User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(default)
|
||||
.ReturnsForAnyArgs(null as Organization);
|
||||
|
||||
// Act
|
||||
var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationDuo_Success(
|
||||
User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
SetupCheckOrganizationAsyncToPass(sutProvider, organization);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<TwoFactorDuoResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutOrganizationDuo_InvalidConfiguration_ThrowsBadRequestException(
|
||||
User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
SetupCheckOrganizationAsyncToPass(sutProvider, organization);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.ValidateDuoConfiguration(default, default, default)
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutOrganizationDuo_Success(
|
||||
User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupCheckAsyncToPass(sutProvider, user);
|
||||
SetupCheckOrganizationAsyncToPass(sutProvider, organization);
|
||||
organization.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.ValidateDuoConfiguration(default, default, default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<TwoFactorDuoResponseModel>(result);
|
||||
Assert.Equal(organization.TwoFactorProviders, request.ToOrganization(organization).TwoFactorProviders);
|
||||
}
|
||||
|
||||
|
||||
private string GetUserTwoFactorDuoProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetOrganizationTwoFactorDuoProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the CheckAsync method to pass.
|
||||
/// </summary>
|
||||
/// <param name="sutProvider">uses bit auto data</param>
|
||||
/// <param name="user">uses bit auto data</param>
|
||||
private void SetupCheckAsyncToPass(SutProvider<TwoFactorController> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.VerifySecretAsync(default, default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
}
|
||||
|
||||
private void SetupCheckOrganizationAsyncToPass(SutProvider<TwoFactorController> sutProvider, Organization organization)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(default)
|
||||
.ReturnsForAnyArgs(organization);
|
||||
}
|
||||
}
|
@ -18,8 +18,6 @@ public class OrganizationTwoFactorDuoRequestModelTests
|
||||
{
|
||||
ClientId = "clientId",
|
||||
ClientSecret = "clientSecret",
|
||||
IntegrationKey = "integrationKey",
|
||||
SecretKey = "secretKey",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
@ -30,8 +28,6 @@ public class OrganizationTwoFactorDuoRequestModelTests
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);
|
||||
}
|
||||
@ -49,8 +45,6 @@ public class OrganizationTwoFactorDuoRequestModelTests
|
||||
{
|
||||
ClientId = "newClientId",
|
||||
ClientSecret = "newClientSecret",
|
||||
IntegrationKey = "newIntegrationKey",
|
||||
SecretKey = "newSecretKey",
|
||||
Host = "newExample.com"
|
||||
};
|
||||
|
||||
@ -61,61 +55,7 @@ public class OrganizationTwoFactorDuoRequestModelTests
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));
|
||||
Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]);
|
||||
Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]);
|
||||
Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]);
|
||||
Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var existingOrg = new Organization();
|
||||
var model = new UpdateTwoFactorDuoRequestModel
|
||||
{
|
||||
IntegrationKey = "integrationKey",
|
||||
SecretKey = "secretKey",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToOrganization(existingOrg);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));
|
||||
Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]);
|
||||
Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]);
|
||||
Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var existingOrg = new Organization();
|
||||
var model = new UpdateTwoFactorDuoRequestModel
|
||||
{
|
||||
ClientId = "clientId",
|
||||
ClientSecret = "clientSecret",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToOrganization(existingOrg);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);
|
||||
}
|
||||
}
|
||||
|
@ -39,12 +39,9 @@ public class TwoFactorDuoRequestModelValidationTests
|
||||
var result = model.Validate(new ValidationContext(model));
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Neither v2 or v4 values are valid.", result.First().ErrorMessage);
|
||||
Assert.Contains("ClientId", result.First().MemberNames);
|
||||
Assert.Contains("ClientSecret", result.First().MemberNames);
|
||||
Assert.Contains("IntegrationKey", result.First().MemberNames);
|
||||
Assert.Contains("SecretKey", result.First().MemberNames);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.Select(x => x.MemberNames.Contains("ClientId")).Any());
|
||||
Assert.True(result.Select(x => x.MemberNames.Contains("ClientSecret")).Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -17,8 +17,6 @@ public class UserTwoFactorDuoRequestModelTests
|
||||
{
|
||||
ClientId = "clientId",
|
||||
ClientSecret = "clientSecret",
|
||||
IntegrationKey = "integrationKey",
|
||||
SecretKey = "secretKey",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
@ -26,12 +24,9 @@ public class UserTwoFactorDuoRequestModelTests
|
||||
var result = model.ToUser(existingUser);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);
|
||||
}
|
||||
@ -49,8 +44,6 @@ public class UserTwoFactorDuoRequestModelTests
|
||||
{
|
||||
ClientId = "newClientId",
|
||||
ClientSecret = "newClientSecret",
|
||||
IntegrationKey = "newIntegrationKey",
|
||||
SecretKey = "newSecretKey",
|
||||
Host = "newExample.com"
|
||||
};
|
||||
|
||||
@ -58,65 +51,10 @@ public class UserTwoFactorDuoRequestModelTests
|
||||
var result = model.ToUser(existingUser);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));
|
||||
Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]);
|
||||
Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]);
|
||||
Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]);
|
||||
Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var existingUser = new User();
|
||||
var model = new UpdateTwoFactorDuoRequestModel
|
||||
{
|
||||
IntegrationKey = "integrationKey",
|
||||
SecretKey = "secretKey",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToUser(existingUser);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));
|
||||
Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]);
|
||||
Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]);
|
||||
Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var existingUser = new User();
|
||||
var model = new UpdateTwoFactorDuoRequestModel
|
||||
{
|
||||
ClientId = "clientId",
|
||||
ClientSecret = "clientSecret",
|
||||
Host = "example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToUser(existingUser);
|
||||
|
||||
// Assert
|
||||
// IKey and SKey should be the same as ClientId and ClientSecret
|
||||
Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]);
|
||||
Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]);
|
||||
Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]);
|
||||
Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]);
|
||||
Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);
|
||||
}
|
||||
}
|
||||
|
@ -8,42 +8,6 @@ namespace Bit.Api.Test.Auth.Models.Response;
|
||||
|
||||
public class OrganizationTwoFactorDuoResponseModelTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Organization_WithDuoV4_ShouldBuildModel(Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV4ProvidersJson();
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
|
||||
// Assert if v4 data Ikey and Skey are set to clientId and clientSecret
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("clientId", model.ClientId);
|
||||
Assert.Equal("secret************", model.ClientSecret);
|
||||
Assert.Equal("clientId", model.IntegrationKey);
|
||||
Assert.Equal("secret************", model.SecretKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Organization_WithDuoV2_ShouldBuildModel(Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV2ProvidersJson();
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
|
||||
// Assert if only v2 data clientId and clientSecret are set to Ikey and Sk
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("IKey", model.ClientId);
|
||||
Assert.Equal("SKey", model.ClientSecret);
|
||||
Assert.Equal("IKey", model.IntegrationKey);
|
||||
Assert.Equal("SKey", model.SecretKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Organization_WithDuo_ShouldBuildModel(Organization organization)
|
||||
@ -54,12 +18,10 @@ public class OrganizationTwoFactorDuoResponseModelTests
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
|
||||
/// Assert Even if both versions are present priority is given to v4 data
|
||||
// Assert
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("clientId", model.ClientId);
|
||||
Assert.Equal("secret************", model.ClientSecret);
|
||||
Assert.Equal("clientId", model.IntegrationKey);
|
||||
Assert.Equal("secret************", model.SecretKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -72,38 +34,33 @@ public class OrganizationTwoFactorDuoResponseModelTests
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
|
||||
/// Assert
|
||||
// Assert
|
||||
Assert.False(model.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization)
|
||||
public void Organization_WithTwoFactorProvidersNull_ShouldThrow(Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = "{\"6\" : {}}";
|
||||
organization.TwoFactorProviders = null;
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
try
|
||||
{
|
||||
var model = new TwoFactorDuoResponseModel(organization);
|
||||
|
||||
/// Assert
|
||||
Assert.False(model.Enabled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Assert
|
||||
Assert.IsType<ArgumentNullException>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTwoFactorOrganizationDuoProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetTwoFactorOrganizationDuoV4ProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetTwoFactorOrganizationDuoV2ProvidersJson()
|
||||
{
|
||||
return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
}
|
||||
|
@ -10,38 +10,21 @@ public class UserTwoFactorDuoResponseModelTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void User_WithDuoV4_ShouldBuildModel(User user)
|
||||
public void User_WithDuo_UserNull_ThrowsArgumentException(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetTwoFactorDuoV4ProvidersJson();
|
||||
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(user);
|
||||
|
||||
// Assert if v4 data Ikey and Skey are set to clientId and clientSecret
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("clientId", model.ClientId);
|
||||
Assert.Equal("secret************", model.ClientSecret);
|
||||
Assert.Equal("clientId", model.IntegrationKey);
|
||||
Assert.Equal("secret************", model.SecretKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void User_WithDuov2_ShouldBuildModel(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetTwoFactorDuoV2ProvidersJson();
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(user);
|
||||
|
||||
// Assert if only v2 data clientId and clientSecret are set to Ikey and Skey
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("IKey", model.ClientId);
|
||||
Assert.Equal("SKey", model.ClientSecret);
|
||||
Assert.Equal("IKey", model.IntegrationKey);
|
||||
Assert.Equal("SKey", model.SecretKey);
|
||||
try
|
||||
{
|
||||
var model = new TwoFactorDuoResponseModel(null as User);
|
||||
}
|
||||
catch (ArgumentNullException e)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -54,12 +37,10 @@ public class UserTwoFactorDuoResponseModelTests
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(user);
|
||||
|
||||
// Assert Even if both versions are present priority is given to v4 data
|
||||
// Assert
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("clientId", model.ClientId);
|
||||
Assert.Equal("secret************", model.ClientSecret);
|
||||
Assert.Equal("clientId", model.IntegrationKey);
|
||||
Assert.Equal("secret************", model.SecretKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -84,26 +65,23 @@ public class UserTwoFactorDuoResponseModelTests
|
||||
user.TwoFactorProviders = null;
|
||||
|
||||
// Act
|
||||
var model = new TwoFactorDuoResponseModel(user);
|
||||
try
|
||||
{
|
||||
var model = new TwoFactorDuoResponseModel(user);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Assert
|
||||
Assert.IsType<ArgumentNullException>(ex);
|
||||
|
||||
}
|
||||
|
||||
/// Assert
|
||||
Assert.False(model.Enabled);
|
||||
}
|
||||
|
||||
private string GetTwoFactorDuoProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetTwoFactorDuoV4ProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetTwoFactorDuoV2ProvidersJson()
|
||||
{
|
||||
return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ namespace Bit.Test.Common.AutoFixture.Attributes;
|
||||
public abstract class BitCustomizeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// /// Gets a customization for the method's parameters.
|
||||
/// Gets a customization for the method's parameters.
|
||||
/// </summary>
|
||||
/// <returns>A customization for the method's paramters.</returns>
|
||||
/// <returns>A customization for the method's parameters.</returns>
|
||||
public abstract ICustomization GetCustomization();
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
@ -19,7 +19,6 @@ public abstract class BaseTokenProviderTests<T>
|
||||
{
|
||||
public abstract TwoFactorProviderType TwoFactorProviderType { get; }
|
||||
|
||||
#region Helpers
|
||||
protected static IEnumerable<object[]> SetupCanGenerateData(params (Dictionary<string, object> MetaData, bool ExpectedResponse)[] data)
|
||||
{
|
||||
return data.Select(d =>
|
||||
@ -48,6 +47,9 @@ public abstract class BaseTokenProviderTests<T>
|
||||
userService
|
||||
.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user)
|
||||
.Returns(true);
|
||||
userService
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
protected static UserManager<User> SubstituteUserManager()
|
||||
@ -76,7 +78,6 @@ public abstract class BaseTokenProviderTests<T>
|
||||
|
||||
user.TwoFactorProviders = JsonHelpers.LegacySerialize(providers);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public virtual async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,
|
||||
User user, SutProvider<T> sutProvider)
|
||||
|
@ -0,0 +1,262 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Identity;
|
||||
|
||||
public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests<DuoUniversalTokenProvider>
|
||||
{
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();
|
||||
public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo;
|
||||
|
||||
public static IEnumerable<object[]> CanGenerateTwoFactorTokenAsyncData
|
||||
=> SetupCanGenerateData(
|
||||
( // correct data
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientId"] = new string('c', 20),
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "https://api-abcd1234.duosecurity.com",
|
||||
},
|
||||
true
|
||||
),
|
||||
( // correct data duo federal
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientId"] = new string('c', 20),
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "https://api-abcd1234.duofederal.com",
|
||||
},
|
||||
true
|
||||
),
|
||||
( // correct data duo federal
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientId"] = new string('c', 20),
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "https://api-abcd1234.duofederal.com",
|
||||
},
|
||||
true
|
||||
),
|
||||
( // invalid host
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientId"] = new string('c', 20),
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "",
|
||||
},
|
||||
false
|
||||
),
|
||||
( // clientId missing
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "https://api-abcd1234.duofederal.com",
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
public static IEnumerable<object[]> NonPremiumCanGenerateTwoFactorTokenAsyncData
|
||||
=> SetupCanGenerateData(
|
||||
( // correct data
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["ClientId"] = new string('c', 20),
|
||||
["ClientSecret"] = new string('s', 40),
|
||||
["Host"] = "https://api-abcd1234.duosecurity.com",
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
[Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))]
|
||||
public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(expectedResponse);
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitMemberAutoData(nameof(NonPremiumCanGenerateTwoFactorTokenAsyncData))]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_UserCanNotAccessPremium_ReturnsNull(Dictionary<string, object> metaData, bool expectedResponse,
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(expectedResponse);
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateToken_Success_ReturnsAuthUrl(
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string authUrl)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperDuoUniversalTokenService(user, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.GenerateAuthUrl(
|
||||
Arg.Any<Duo.Client>(),
|
||||
Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),
|
||||
user)
|
||||
.Returns(authUrl);
|
||||
|
||||
// Act
|
||||
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(token);
|
||||
Assert.Equal(token, authUrl);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateToken_DuoClientNull_ReturnsNull(
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
|
||||
AdditionalSetup(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(null as Duo.Client);
|
||||
|
||||
// Act
|
||||
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
|
||||
|
||||
// Assert
|
||||
Assert.Null(token);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateToken_UserCanNotAccessPremium_ReturnsNull(
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
|
||||
AdditionalSetup(sutProvider, user);
|
||||
|
||||
// Act
|
||||
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
|
||||
|
||||
// Assert
|
||||
Assert.Null(token);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateToken_ValidToken_ReturnsTrue(
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperDuoUniversalTokenService(user, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.RequestDuoValidationAsync(
|
||||
Arg.Any<Duo.Client>(),
|
||||
Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),
|
||||
user,
|
||||
token)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var response = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user);
|
||||
|
||||
// Assert
|
||||
Assert.True(response);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateToken_DuoClientNull_ReturnsFalse(
|
||||
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)
|
||||
{
|
||||
user.Premium = true;
|
||||
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
|
||||
AdditionalSetup(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(null as Duo.Client);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the IDuoUniversalTokenService is properly setup for the test.
|
||||
/// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider
|
||||
/// methods will return true enabling the test to execute on the correct path.
|
||||
/// </summary>
|
||||
/// <param name="user">user from calling test</param>
|
||||
/// <param name="sutProvider">self</param>
|
||||
private void SetUpProperDuoUniversalTokenService(User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
user.Premium = true;
|
||||
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
|
||||
var client = BuildDuoClient();
|
||||
|
||||
AdditionalSetup(sutProvider, user);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(client);
|
||||
}
|
||||
|
||||
private Duo.Client BuildDuoClient()
|
||||
{
|
||||
var clientId = new string('c', 20);
|
||||
var clientSecret = new string('s', 40);
|
||||
return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build();
|
||||
}
|
||||
|
||||
private string GetTwoFactorDuoProvidersJson()
|
||||
{
|
||||
return
|
||||
"{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
@ -0,0 +1,289 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Identity;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationDuoUniversalTwoFactorTokenProviderTests
|
||||
{
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<DuoUserStateTokenable>>();
|
||||
|
||||
// Happy path
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_ReturnsTrue(
|
||||
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Enabled = true;
|
||||
organization.Use2fa = true;
|
||||
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_DuoTwoFactorNotEnabled_ReturnsFalse(
|
||||
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderNotEnabledJson();
|
||||
organization.Use2fa = true;
|
||||
organization.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_BadMetaData_ProviderNull_ReturnsFalse(
|
||||
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Use2fa = true;
|
||||
organization.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(false);
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetDuoTwoFactorProvider_OrganizationNull_ReturnsNull(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetDuoTwoFactorProvider_OrganizationNotEnabled_ReturnsNull(
|
||||
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
|
||||
organization.Enabled = false;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetDuoTwoFactorProvider_OrganizationUse2FAFalse_ReturnsNull(
|
||||
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
|
||||
organization.Use2fa = false;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetDuoClient_ProviderNull_ReturnsNull(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GenerateAsync(null, default);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetDuoClient_DuoClientNull_ReturnsNull(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Use2fa = true;
|
||||
organization.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(null as Duo.Client);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GenerateAsync(organization, default);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateAsync_ReturnsAuthUrl(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
|
||||
Organization organization,
|
||||
User user,
|
||||
string AuthUrl)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.GenerateAuthUrl(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user)
|
||||
.Returns(AuthUrl);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GenerateAsync(organization, user);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(AuthUrl, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateAsync_ClientNull_ReturnsNull(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
|
||||
Organization organization,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GenerateAsync(organization, user);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_TokenValid_ReturnsTrue(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
|
||||
Organization organization,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(token, organization, user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_ClientNull_ReturnsFalse(
|
||||
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
|
||||
Organization organization,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(token, organization, user);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the IDuoUniversalTokenService is properly setup for the test.
|
||||
/// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider
|
||||
/// methods will return true enabling the test to execute on the correct path.
|
||||
///
|
||||
/// BitAutoData cannot create the Duo.Client since it does not have a public constructor
|
||||
/// so we have to use the ClientBUilder(), the client is not used meaningfully in the tests.
|
||||
/// </summary>
|
||||
/// <param name="user">user from calling test</param>
|
||||
/// <param name="sutProvider">self</param>
|
||||
private void SetUpProperOrganizationDuoUniversalTokenService(
|
||||
Duo.Client client, Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
|
||||
{
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
organization.Use2fa = true;
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDuoUniversalTokenService>()
|
||||
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
|
||||
.Returns(client);
|
||||
}
|
||||
|
||||
private Duo.Client BuildDuoClient()
|
||||
{
|
||||
var clientId = new string('c', 20);
|
||||
var clientSecret = new string('s', 40);
|
||||
return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build();
|
||||
}
|
||||
|
||||
private string GetTwoFactorOrganizationDuoProviderJson()
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private string GetTwoFactorOrganizationDuoProviderNotEnabledJson()
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DuoUniversalTokenServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData("", "ClientId", "ClientSecret")]
|
||||
[BitAutoData("api-valid.duosecurity.com", "", "ClientSecret")]
|
||||
[BitAutoData("api-valid.duosecurity.com", "ClientId", "")]
|
||||
public async void ValidateDuoConfiguration_InvalidConfig_ReturnsFalse(
|
||||
string host, string clientId, string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
/* AutoData handles arrangement */
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateDuoConfiguration(clientSecret, clientId, host);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true, "api-valid.duosecurity.com")]
|
||||
[BitAutoData(false, "invalid")]
|
||||
[BitAutoData(false, "api-valid.duosecurity.com", null, "clientSecret")]
|
||||
[BitAutoData(false, "api-valid.duosecurity.com", "ClientId", null)]
|
||||
[BitAutoData(false, "api-valid.duosecurity.com", null, null)]
|
||||
public void HasProperDuoMetadata_ReturnMatchesExpected(
|
||||
bool expectedResponse, string host, string clientId,
|
||||
string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var metaData = new Dictionary<string, object> { ["Host"] = host };
|
||||
|
||||
if (clientId != null)
|
||||
{
|
||||
metaData.Add("ClientId", clientId);
|
||||
}
|
||||
|
||||
if (clientSecret != null)
|
||||
{
|
||||
metaData.Add("ClientSecret", clientSecret);
|
||||
}
|
||||
|
||||
var provider = new TwoFactorProvider
|
||||
{
|
||||
MetaData = metaData
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.HasProperDuoMetadata(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result, expectedResponse);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void HasProperDuoMetadata_ProviderIsNull_ReturnsFalse(
|
||||
SutProvider<DuoUniversalTokenService> sutProvider)
|
||||
{
|
||||
// Act
|
||||
var result = sutProvider.Sut.HasProperDuoMetadata(null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("api-valid.duosecurity.com", true)]
|
||||
[BitAutoData("api-valid.duofederal.com", true)]
|
||||
[BitAutoData("invalid", false)]
|
||||
public void ValidDuoHost_HostIsValid_ReturnTrue(
|
||||
string host, bool expectedResponse)
|
||||
{
|
||||
// Act
|
||||
var result = DuoUniversalTokenService.ValidDuoHost(host);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result, expectedResponse);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -28,7 +28,7 @@ namespace Bit.Identity.IntegrationTest.Endpoints;
|
||||
|
||||
public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"IKey":"DIEFB13LB49IEB3459N2","SKey":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}""";
|
||||
const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"ClientId":"DIEFB13LB49IEB3459N2","ClientSecret":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}""";
|
||||
const string _testEmail = "test+2farequired@email.com";
|
||||
const string _testPassword = "master_password_hash";
|
||||
const string _userEmailTwoFactor = """{"1": { "Enabled": true, "MetaData": { "Email": "test+2farequired@email.com"}}}""";
|
||||
@ -140,7 +140,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "password", _testPassword },
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = responseBody.RootElement;
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
||||
@ -168,7 +168,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
[Theory, BitAutoData]
|
||||
public async Task TokenEndpoint_GrantTypeClientCredential_OrgTwoFactorRequired_Success(Organization organization, OrganizationApiKey organizationApiKey)
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
organization.Enabled = true;
|
||||
organization.UseApi = true;
|
||||
organization.Use2fa = true;
|
||||
@ -258,7 +258,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = responseBody.RootElement;
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
||||
@ -320,7 +320,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(twoFactorProvidedContext);
|
||||
var root = body.RootElement;
|
||||
|
||||
@ -338,6 +338,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.MasterPassword,
|
||||
};
|
||||
|
||||
await CreateSsoOrganizationAndUserAsync(
|
||||
localFactory, ssoConfigData, challenge, _testEmail, orgTwoFactor: _organizationTwoFactor);
|
||||
|
||||
@ -355,7 +356,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = responseBody.RootElement;
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
||||
|
@ -1,8 +1,6 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -28,8 +26,7 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly UserManagerTestWrapper<User> _userManager;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService;
|
||||
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
@ -42,8 +39,7 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_userManager = SubstituteUserManager();
|
||||
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
|
||||
_temporaryDuoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
|
||||
_organizationDuoUniversalTokenProvider = Substitute.For<IOrganizationDuoUniversalTokenProvider>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_applicationCacheService = Substitute.For<IApplicationCacheService>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
@ -54,8 +50,7 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
_sut = new TwoFactorAuthenticationValidator(
|
||||
_userService,
|
||||
_userManager,
|
||||
_organizationDuoWebTokenProvider,
|
||||
_temporaryDuoWebV4SDKService,
|
||||
_organizationDuoUniversalTokenProvider,
|
||||
_featureService,
|
||||
_applicationCacheService,
|
||||
_organizationUserRepository,
|
||||
@ -439,7 +434,7 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_organizationDuoWebTokenProvider.ValidateAsync(
|
||||
_organizationDuoUniversalTokenProvider.ValidateAsync(
|
||||
token, organization, user).Returns(true);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
@ -457,70 +452,6 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
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>(
|
||||
|
Loading…
Reference in New Issue
Block a user