1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

[PM-4614] Updating Duo to SDK v4 for Universal Prompt (#3664)

* added v4 updates

* Fixed packages.

* Null checks and OrganizationDuo

* enable backwards compatibility support

* updated validation

* Update DuoUniversalPromptService.cs

add JIRA ticket for cleanup

* Update BaseRequestValidator.cs

* updates to names and comments

* fixed tests

* fixed validation errros and authURL

* updated naming

* Filename change

* Update BaseRequestValidator.cs
This commit is contained in:
Ike 2024-01-24 10:13:00 -08:00 committed by GitHub
parent 7577da083c
commit 0deb13791a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 180 additions and 9 deletions

View File

@ -0,0 +1,121 @@
using Bit.Core.Auth.Models;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Settings;
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;
/// <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)
{
_currentContext = currentContext;
_globalSettings = globalSettings;
}
/// <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))
{
return null;
}
var duoClient = await BuildDuoClientAsync(provider);
if (duoClient == null)
{
return null;
}
var state = Duo.Client.GenerateState(); //? Not sure on this yet. But required for GenerateAuthUrl
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))
{
return false;
}
var duoClient = await BuildDuoClientAsync(provider);
if (duoClient == null)
{
return false;
}
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(token, user.Email);
return res.AuthResult.Result == "allow";
}
private bool HasProperMetaData(TwoFactorProvider provider)
{
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
}
/// <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 correct 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["IKey"],
(string)provider.MetaData["SKey"],
(string)provider.MetaData["Host"],
redirectUri).Build();
if (!await client.DoHealthCheck())
{
return null;
}
return client;
}
}

View File

@ -105,6 +105,7 @@ public static class FeatureFlagKeys
public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share";
public const string KeyRotationImprovements = "key-rotation-improvements";
public const string DuoRedirect = "duo-redirect";
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string FlexibleCollectionsSignup = "flexible-collections-signup";

View File

@ -28,6 +28,7 @@
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
<PackageReference Include="Azure.Storage.Queues" Version="12.12.0" />
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
<PackageReference Include="DuoUniversal" Version="1.2.1" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.4" />

View File

@ -38,6 +38,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IDeviceService _deviceService;
private readonly IEventService _eventService;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
@ -63,6 +64,7 @@ public abstract class BaseRequestValidator<T> where T : class
IUserService userService,
IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
@ -84,6 +86,7 @@ public abstract class BaseRequestValidator<T> where T : class
_userService = userService;
_eventService = eventService;
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
_duoWebV4SDKService = duoWebV4SDKService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
@ -167,7 +170,7 @@ public abstract class BaseRequestValidator<T> where T : class
}
return;
}
// We only want to track TOTPs in the chache to enforce one time use.
// We only want to track TOTPs in the cache to enforce one time use.
if (twoFactorProviderType == TwoFactorProviderType.Authenticator || twoFactorProviderType == TwoFactorProviderType.Email)
{
await Core.Utilities.DistributedCacheExtensions.SetAsync(_distributedCache, cacheKey, twoFactorToken, _cacheEntryOptions);
@ -428,10 +431,23 @@ public abstract class BaseRequestValidator<T> where T : class
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Remember:
if (type != TwoFactorProviderType.Remember &&
!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return false;
}
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (type == TwoFactorProviderType.Duo)
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
}
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token);
@ -441,6 +457,20 @@ public abstract class BaseRequestValidator<T> where T : class
return false;
}
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
if (type == TwoFactorProviderType.OrganizationDuo)
{
if (!token.Contains(':'))
{
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
}
}
}
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
default:
return false;
@ -465,11 +495,19 @@ public abstract class BaseRequestValidator<T> where T : class
CoreHelpers.CustomProviderName(type));
if (type == TwoFactorProviderType.Duo)
{
return new Dictionary<string, object>
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = token
};
// DUO SDK v4 Update: Duo-Redirect
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
// Generate AuthUrl from DUO SDK v4 token provider
duoResponse.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
}
return duoResponse;
}
else if (type == TwoFactorProviderType.WebAuthn)
{
@ -493,13 +531,19 @@ public abstract class BaseRequestValidator<T> where T : class
case TwoFactorProviderType.OrganizationDuo:
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
{
return new Dictionary<string, object>
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
};
// DUO SDK v4 Update: DUO-Redirect
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
{
// Generate AuthUrl from DUO SDK v4 token provider
duoResponse.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
}
return duoResponse;
}
return null;
default:
return null;

View File

@ -33,6 +33,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IUserService userService,
IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
@ -48,7 +49,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
distributedCache, userDecryptionOptionsBuilder)

View File

@ -34,6 +34,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IUserService userService,
IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
@ -51,7 +52,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder)
{

View File

@ -36,6 +36,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IUserService userService,
IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
@ -54,7 +55,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder)
{

View File

@ -376,7 +376,8 @@ public static class ServiceCollectionExtensions
public static IdentityBuilder AddCustomIdentityServices(
this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddSingleton<IOrganizationDuoWebTokenProvider, OrganizationDuoWebTokenProvider>();
services.AddScoped<IOrganizationDuoWebTokenProvider, OrganizationDuoWebTokenProvider>();
services.AddScoped<ITemporaryDuoWebV4SDKService, TemporaryDuoWebV4SDKService>();
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
{