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:
@ -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.");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user