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;
///
/// OrganizationDuo and Duo TwoFactorProviderTypes both use the same flows so both of those Token Providers will
/// have this class injected to utilize these methods
///
public interface IDuoUniversalTokenService
{
///
/// 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.
///
/// A not null valid Duo.Client
/// This service creates the state token for added security
/// currently active user
/// a URL in string format
string GenerateAuthUrl(
Duo.Client duoClient,
IDataProtectorTokenFactory tokenDataFactory,
User user);
///
/// Makes the request to Duo to validate the authCode and state token
///
/// A not null valid Duo.Client
/// Factory for decrypting the state
/// self
/// token received from the client
/// boolean based on result from Duo
Task RequestDuoValidationAsync(
Duo.Client duoClient,
IDataProtectorTokenFactory tokenDataFactory,
User user,
string token);
///
/// 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.
///
/// Duo client Secret
/// Duo client Id
/// Duo host
/// Boolean
Task ValidateDuoConfiguration(string clientSecret, string clientId, string host);
///
/// 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.
///
/// Host being checked for proper data
/// true if all three are present; false if one is missing or the host is incorrect
bool HasProperDuoMetadata(TwoFactorProvider provider);
///
/// 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.
///
/// TwoFactorProvider Duo or OrganizationDuo
/// Duo.Client object or null
Task 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 tokenDataFactory,
User user)
{
var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user));
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
return authUrl;
}
public async Task RequestDuoValidationAsync(
Duo.Client duoClient,
IDataProtectorTokenFactory 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 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"]);
}
///
/// Checks the host string to make sure it meets Duo's Guidelines before attempting to create a Duo.Client.
///
/// string representing the Duo Host
/// true if the host is valid false otherwise
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 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;
}
}