1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-5216] User and Organization Duo Request and Response Model refactor (#4126)

* inital changes

* add provider GatewayType migrations

* db provider migrations

* removed duo migrations added v2 metadata to duo response

* removed helper scripts

* remove signature from org duo

* added backward compatibility for Duo v2

* added tests for duo request + response models

* refactors to TwoFactorController

* updated test methods to be compartmentalized by usage

* fix organization add duo

* Assert.Empty() fix for validator
This commit is contained in:
Ike
2024-06-05 11:42:02 -07:00
committed by GitHub
parent a0a7654077
commit 97b3f3e7ee
19 changed files with 8504 additions and 55 deletions

View File

@ -159,7 +159,16 @@ public class TwoFactorController : Controller
var user = await CheckAsync(model, true);
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
// 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)
@ -178,7 +187,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
[FromBody] SecretVerificationRequestModel model)
{
var user = await CheckAsync(model, false);
await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -186,12 +195,7 @@ public class TwoFactorController : Controller
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();
var response = new TwoFactorDuoResponseModel(organization);
return response;
}
@ -201,7 +205,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
[FromBody] UpdateTwoFactorDuoRequestModel model)
{
var user = await CheckAsync(model, false);
await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -209,15 +213,19 @@ public class TwoFactorController : Controller
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
// 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)
@ -439,7 +447,7 @@ public class TwoFactorController : Controller
throw new BadRequestException(string.Empty, "User verification failed.");
}
if (premium && !(await _userService.CanAccessPremium(user)))
if (premium && !await _userService.CanAccessPremium(user))
{
throw new BadRequestException("Premium status is required.");
}

View File

@ -42,10 +42,18 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
{
[Required]
/*
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.
*/
[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; }
[Required]
//todo - will remove SKey and IKey with PM-8107
[StringLength(50)]
public string SecretKey { get; set; }
[Required]
@ -64,12 +72,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
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
},
Enabled = true
@ -90,12 +103,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
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
},
Enabled = true
@ -108,7 +126,31 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
{
if (!DuoApi.ValidHost(Host))
{
yield return new ValidationResult("Host is invalid.", new string[] { nameof(Host) });
yield return new ValidationResult("Host is invalid.", [nameof(Host)]);
}
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))
{
SecretKey = ClientSecret;
IntegrationKey = ClientId;
}
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
{
ClientSecret = SecretKey;
ClientId = IntegrationKey;
}
}
}

View File

@ -36,26 +36,54 @@ public class TwoFactorDuoResponseModel : ResponseModel
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)
{
Enabled = provider.Enabled;
if (provider.MetaData.ContainsKey("Host"))
if (provider.MetaData.TryGetValue("Host", out var host))
{
Host = (string)provider.MetaData["Host"];
Host = (string)host;
}
if (provider.MetaData.ContainsKey("SKey"))
//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))
{
SecretKey = (string)provider.MetaData["SKey"];
ClientSecret = (string)sKey;
SecretKey = (string)sKey;
}
if (provider.MetaData.ContainsKey("IKey"))
if (provider.MetaData.TryGetValue("IKey", out var iKey))
{
IntegrationKey = (string)provider.MetaData["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 = (string)clientSecret;
SecretKey = (string)clientSecret;
}
}
if (provider.MetaData.TryGetValue("ClientId", out var clientId))
{
if (!string.IsNullOrWhiteSpace((string)clientId))
{
ClientId = (string)clientId;
IntegrationKey = (string)clientId;
}
}
}
else
@ -63,4 +91,27 @@ public class TwoFactorDuoResponseModel : ResponseModel
Enabled = false;
}
}
/*
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.");
}
}
}

View File

@ -110,8 +110,8 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
private bool HasProperMetaData(TwoFactorProvider provider)
{
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") &&
provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host");
}
/// <summary>
@ -122,14 +122,14 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
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
// 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["IKey"],
(string)provider.MetaData["SKey"],
(string)provider.MetaData["ClientId"],
(string)provider.MetaData["ClientSecret"],
(string)provider.MetaData["Host"],
redirectUri).Build();

View File

@ -483,7 +483,7 @@ public abstract class BaseRequestValidator<T> where T : class
case TwoFactorProviderType.WebAuthn:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.YubiKey:
if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
{
return null;
}
@ -495,15 +495,9 @@ public abstract class BaseRequestValidator<T> where T : class
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = token
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, 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;
}
else if (type == TwoFactorProviderType.WebAuthn)
@ -531,14 +525,9 @@ public abstract class BaseRequestValidator<T> where T : class
var duoResponse = new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, 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;