diff --git a/MultiFactor.Ldap.Adapter/Configuration/ServiceConfiguration.cs b/MultiFactor.Ldap.Adapter/Configuration/ServiceConfiguration.cs
index 1ce1186..89cbd77 100644
--- a/MultiFactor.Ldap.Adapter/Configuration/ServiceConfiguration.cs
+++ b/MultiFactor.Ldap.Adapter/Configuration/ServiceConfiguration.cs
@@ -1,5 +1,5 @@
//Copyright(c) 2021 MultiFactor
-//Please see licence at
+//Please see licence at
//https://github.com/MultifactorLab/MultiFactor.Ldap.Adapter/blob/main/LICENSE.md
using MultiFactor.Ldap.Adapter.Core;
@@ -60,7 +60,7 @@ public ClientConfiguration GetClient(IPAddress ip)
///
/// Multifactor API URL
///
- public string ApiUrl { get; set; }
+ public string[] ApiUrls { get; set; }
///
/// HTTP Proxy for API
@@ -115,9 +115,14 @@ public static ServiceConfiguration Load(ILogger logger)
throw new Exception("Configuration error: 'logging-level' element not found");
}
+ var apiUrls = apiUrlSetting
+ .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim())
+ .Distinct()
+ .ToArray();
var configuration = new ServiceConfiguration
{
- ApiUrl = apiUrlSetting,
+ ApiUrls = apiUrls,
ApiProxy = apiProxySetting,
ApiTimeout = apiTimeout,
LogLevel = logLevelSetting,
@@ -231,7 +236,7 @@ private static ClientConfiguration Load(string name, AppSettingsSection appSetti
LdapServer = ldapServerSetting,
MultifactorApiKey = multifactorApiKeySetting,
MultifactorApiSecret = multifactorApiSecretSetting,
- TransformLdapIdentity = string.IsNullOrEmpty(transformLdapIdentity)
+ TransformLdapIdentity = string.IsNullOrEmpty(transformLdapIdentity)
? LdapIdentityFormat.None
: (LdapIdentityFormat)Enum.Parse(typeof(LdapIdentityFormat), transformLdapIdentity, true)
};
@@ -307,7 +312,7 @@ private static ClientConfiguration Load(string name, AppSettingsSection appSetti
{
throw new Exception($"Configuration error: Can't parse '{Constants.Configuration.AuthenticationCacheLifetime}' value");
}
-
+
if (TimeSpan.TryParse(ldapBindTimeout, out var bindTimeout))
{
if (bindTimeout > TimeSpan.Zero)
diff --git a/MultiFactor.Ldap.Adapter/Extensions/ServiceCollectionExtensions.cs b/MultiFactor.Ldap.Adapter/Extensions/ServiceCollectionExtensions.cs
index ea56902..fe4cf6e 100644
--- a/MultiFactor.Ldap.Adapter/Extensions/ServiceCollectionExtensions.cs
+++ b/MultiFactor.Ldap.Adapter/Extensions/ServiceCollectionExtensions.cs
@@ -12,6 +12,8 @@
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Http.Resilience;
+using Polly;
namespace MultiFactor.Ldap.Adapter.Extensions
{
@@ -48,7 +50,16 @@ public static void AddHttpClientWithProxy(this IServiceCollection services)
return handler;
})
- .AddHttpMessageHandler();
+ .AddHttpMessageHandler()
+ .AddResilienceHandler("mf-api-pipeline", x =>
+ {
+ x.AddRetry(new HttpRetryStrategyOptions
+ {
+ MaxRetryAttempts = 2,
+ Delay = TimeSpan.FromSeconds(1),
+ BackoffType = DelayBackoffType.Exponential
+ });
+ });
}
public static void ConfigureApplicationServices(this IServiceCollection services, LoggingLevelSwitch levelSwitch, string syslogInfoMessage)
diff --git a/MultiFactor.Ldap.Adapter/MultiFactor.Ldap.Adapter.csproj b/MultiFactor.Ldap.Adapter/MultiFactor.Ldap.Adapter.csproj
index 84cc8d1..9df9f5f 100644
--- a/MultiFactor.Ldap.Adapter/MultiFactor.Ldap.Adapter.csproj
+++ b/MultiFactor.Ldap.Adapter/MultiFactor.Ldap.Adapter.csproj
@@ -90,39 +90,106 @@
..\packages\Microsoft.Bcl.AsyncInterfaces.9.0.1\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll
+
+ ..\packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll
+
+
+ ..\packages\Microsoft.Bcl.TimeProvider.8.0.1\lib\net462\Microsoft.Bcl.TimeProvider.dll
+
+
+ ..\packages\Microsoft.Extensions.AmbientMetadata.Application.9.2.0\lib\net462\Microsoft.Extensions.AmbientMetadata.Application.dll
+
+
+ ..\packages\Microsoft.Extensions.Compliance.Abstractions.9.2.0\lib\netstandard2.0\Microsoft.Extensions.Compliance.Abstractions.dll
+
+
+ ..\packages\Microsoft.Extensions.Configuration.8.0.0\lib\net462\Microsoft.Extensions.Configuration.dll
+
..\packages\Microsoft.Extensions.Configuration.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.Configuration.Abstractions.dll
-
- ..\packages\Microsoft.Extensions.DependencyInjection.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.dll
+
+ ..\packages\Microsoft.Extensions.Configuration.Binder.8.0.2\lib\net462\Microsoft.Extensions.Configuration.Binder.dll
+
+
+ ..\packages\Microsoft.Extensions.DependencyInjection.8.0.1\lib\net462\Microsoft.Extensions.DependencyInjection.dll
+
+
+ ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.2\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll
+
+
+ ..\packages\Microsoft.Extensions.DependencyInjection.AutoActivation.9.2.0\lib\net462\Microsoft.Extensions.DependencyInjection.AutoActivation.dll
+
+
+ ..\packages\Microsoft.Extensions.Diagnostics.8.0.1\lib\net462\Microsoft.Extensions.Diagnostics.dll
+
+
+ ..\packages\Microsoft.Extensions.Diagnostics.Abstractions.8.0.1\lib\net462\Microsoft.Extensions.Diagnostics.Abstractions.dll
+
+
+ ..\packages\Microsoft.Extensions.Diagnostics.ExceptionSummarization.9.2.0\lib\net462\Microsoft.Extensions.Diagnostics.ExceptionSummarization.dll
+
+
+ ..\packages\Microsoft.Extensions.FileProviders.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.FileProviders.Abstractions.dll
+
+
+ ..\packages\Microsoft.Extensions.Hosting.Abstractions.8.0.1\lib\net462\Microsoft.Extensions.Hosting.Abstractions.dll
-
- ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll
+
+ ..\packages\Microsoft.Extensions.Http.8.0.1\lib\net462\Microsoft.Extensions.Http.dll
-
- ..\packages\Microsoft.Extensions.Http.8.0.0\lib\net462\Microsoft.Extensions.Http.dll
+
+ ..\packages\Microsoft.Extensions.Http.Diagnostics.9.2.0\lib\net462\Microsoft.Extensions.Http.Diagnostics.dll
-
- ..\packages\Microsoft.Extensions.Logging.8.0.0\lib\net462\Microsoft.Extensions.Logging.dll
+
+ ..\packages\Microsoft.Extensions.Http.Resilience.9.2.0\lib\net462\Microsoft.Extensions.Http.Resilience.dll
-
- ..\packages\Microsoft.Extensions.Logging.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll
+
+ ..\packages\Microsoft.Extensions.Logging.8.0.1\lib\net462\Microsoft.Extensions.Logging.dll
-
- ..\packages\Microsoft.Extensions.ObjectPool.2.2.0\lib\netstandard2.0\Microsoft.Extensions.ObjectPool.dll
+
+ ..\packages\Microsoft.Extensions.Logging.Abstractions.8.0.3\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll
-
- ..\packages\Microsoft.Extensions.Options.8.0.0\lib\net462\Microsoft.Extensions.Options.dll
+
+ ..\packages\Microsoft.Extensions.Logging.Configuration.8.0.1\lib\net462\Microsoft.Extensions.Logging.Configuration.dll
+
+
+ ..\packages\Microsoft.Extensions.ObjectPool.8.0.13\lib\net462\Microsoft.Extensions.ObjectPool.dll
+
+
+ ..\packages\Microsoft.Extensions.Options.8.0.2\lib\net462\Microsoft.Extensions.Options.dll
+
+
+ ..\packages\Microsoft.Extensions.Options.ConfigurationExtensions.8.0.0\lib\net462\Microsoft.Extensions.Options.ConfigurationExtensions.dll
..\packages\Microsoft.Extensions.Primitives.8.0.0\lib\net462\Microsoft.Extensions.Primitives.dll
+
+ ..\packages\Microsoft.Extensions.Resilience.9.2.0\lib\net462\Microsoft.Extensions.Resilience.dll
+
+
+ ..\packages\Microsoft.Extensions.Telemetry.9.2.0\lib\net462\Microsoft.Extensions.Telemetry.dll
+
+
+ ..\packages\Microsoft.Extensions.Telemetry.Abstractions.9.2.0\lib\net462\Microsoft.Extensions.Telemetry.Abstractions.dll
+
..\packages\Microsoft.Net.Http.Headers.2.2.0\lib\netstandard2.0\Microsoft.Net.Http.Headers.dll
+
..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\Polly.Core.8.4.2\lib\net462\Polly.Core.dll
+
+
+ ..\packages\Polly.Extensions.8.4.2\lib\net462\Polly.Extensions.dll
+
+
+ ..\packages\Polly.RateLimiting.8.4.2\lib\net462\Polly.RateLimiting.dll
+
..\packages\Serilog.2.10.0\lib\net46\Serilog.dll
@@ -145,12 +212,18 @@
..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
+
+ ..\packages\System.Collections.Immutable.8.0.0\lib\net462\System.Collections.Immutable.dll
+
+
+ ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll
+
-
- ..\packages\System.Diagnostics.DiagnosticSource.8.0.0\lib\net462\System.Diagnostics.DiagnosticSource.dll
+
+ ..\packages\System.Diagnostics.DiagnosticSource.8.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll
..\packages\System.IO.Pipelines.9.0.1\lib\net462\System.IO.Pipelines.dll
@@ -178,6 +251,9 @@
..\packages\System.Text.Json.9.0.1\lib\net462\System.Text.Json.dll
+
+ ..\packages\System.Threading.RateLimiting.8.0.0\lib\net462\System.Threading.RateLimiting.dll
+
..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll
diff --git a/MultiFactor.Ldap.Adapter/Services/MultiFactorApiClient.cs b/MultiFactor.Ldap.Adapter/Services/MultiFactorApiClient.cs
index 8788fac..38f9dc7 100644
--- a/MultiFactor.Ldap.Adapter/Services/MultiFactorApiClient.cs
+++ b/MultiFactor.Ldap.Adapter/Services/MultiFactorApiClient.cs
@@ -1,5 +1,5 @@
//Copyright(c) 2021 MultiFactor
-//Please see licence at
+//Please see licence at
//https://github.com/MultifactorLab/MultiFactor.Ldap.Adapter/blob/main/LICENSE.md
@@ -12,6 +12,9 @@
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
+using System.Linq;
+using Polly;
+using Polly.Timeout;
namespace MultiFactor.Ldap.Adapter.Services
{
@@ -64,13 +67,12 @@ public async Task Authenticate(ConnectedClientInfo connectedClient)
return true;
}
- var url = _configuration.ApiUrl + "/access/requests/la";
var payload = new
{
Identity = connectedClient.Username,
};
- var response = await SendRequest(connectedClient.ClientConfiguration, url, payload);
+ var response = await SendRequest(connectedClient.ClientConfiguration, _configuration.ApiUrls, payload);
if (response == null)
{
@@ -88,80 +90,96 @@ public async Task Authenticate(ConnectedClientInfo connectedClient)
{
var reason = response?.ReplyMessage;
var phone = response?.Phone;
- _logger.Warning("Second factor verification for user '{user:l}' failed with reason='{reason:l}'. User phone {phone:l}",
+ _logger.Warning("Second factor verification for user '{user:l}' failed with reason='{reason:l}'. User phone {phone:l}",
connectedClient.Username, reason, phone);
}
return response.Granted;
}
- private async Task SendRequest(ClientConfiguration clientConfig, string url, object payload)
+ private async Task SendRequest(ClientConfiguration clientConfig, string[] baseUrls, object payload)
{
- try
- {
- //make sure we can communicate securely
- ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
- ServicePointManager.DefaultConnectionLimit = 100;
+ //make sure we can communicate securely
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
+ ServicePointManager.DefaultConnectionLimit = 100;
- var json = JsonSerializer.Serialize(payload, _serialazerOptions);
+ var json = JsonSerializer.Serialize(payload, _serialazerOptions);
- _logger.Debug($"Sending request to API: {json}");
+ _logger.Debug("Sending request to API: {Body}.", json);
- //basic authorization
- var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(clientConfig.MultifactorApiKey + ":" + clientConfig.MultifactorApiSecret));
- var httpClient = _httpClientFactory.CreateClient(nameof(MultiFactorApiClient));
+ //basic authorization
+ var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientConfig.MultifactorApiKey}:{clientConfig.MultifactorApiSecret}"));
+ var httpClient = _httpClientFactory.CreateClient(nameof(MultiFactorApiClient));
- StringContent jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
- HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, url)
- {
- Content = jsonContent
- };
- message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth);
- var res = await httpClient.SendAsync(message);
-
- if ((int)res.StatusCode == 429)
+ foreach (var url in baseUrls.Select(baseUrl => $"{baseUrl}/access/requests/la"))
+ {
+ _logger.Information("Sending request to API '{ApiUrl:l}'.", url);
+ try
{
- _logger.Warning("Got unsuccessful response from API: {@response}", res.ReasonPhrase);
- return new MultiFactorAccessRequest() { Status = "Denied", ReplyMessage = "Too many requests"};
- }
-
- var jsonResponse = await res.Content.ReadAsStringAsync();
- var response = JsonSerializer.Deserialize>(jsonResponse, _serialazerOptions);
+ var message = CreateHttpRequestMessage(json, url, auth);
+ var res = await TrySendRequestAsync(httpClient, message);
+ if (res == null)
+ continue;
- _logger.Debug("Received response from API: {@response}", response);
+ if ((int)res.StatusCode == 429)
+ {
+ _logger.Warning("Got unsuccessful response from API '{ApiUrl:l}': {@response}", url, res.ReasonPhrase);
+ return new MultiFactorAccessRequest { Status = "Denied", ReplyMessage = "Too many requests" };
+ }
- if (!response.Success)
- {
- _logger.Warning("Got unsuccessful response from API: {@response}", response);
- }
+ var jsonResponse = await res.Content.ReadAsStringAsync();
+ var response = JsonSerializer.Deserialize>(jsonResponse, _serialazerOptions);
- return response.Model;
- }
- catch (TaskCanceledException tce)
- {
- _logger.Error(tce, $"Multifactor API host unreachable {url}: timeout!");
+ _logger.Debug("Received response from API '{ApiUrl:l}': {@response}", url, response);
- if (clientConfig.BypassSecondFactorWhenApiUnreachable)
- {
- _logger.Warning("Bypass second factor");
- return MultiFactorAccessRequest.Bypass;
- }
+ if (!response.Success)
+ {
+ _logger.Warning("Got unsuccessful response from API: {@response}", response);
+ throw new HttpRequestException($"Got unsuccessful response from API. Status code: {res.StatusCode}.");
+ }
- return null;
- }
- catch (Exception ex)
- {
- _logger.Error(ex, $"Multifactor API host unreachable {url}: {ex.Message}");
+ return response.Model;
- if (clientConfig.BypassSecondFactorWhenApiUnreachable)
+ }
+ catch (Exception ex)
{
- _logger.Warning("Bypass second factor");
- return MultiFactorAccessRequest.Bypass;
+ _logger.Error(ex, "Multifactor API host '{ApiUrl:l}' unreachable: {Message:l}", url, ex.Message);
}
+ }
+ _logger.Error("Multifactor API Cloud unreachable");
+
+ if (clientConfig.BypassSecondFactorWhenApiUnreachable)
+ {
+ _logger.Warning("Bypass second factor");
+ return MultiFactorAccessRequest.Bypass;
+ }
+ return null;
+ }
+
+ private async Task TrySendRequestAsync(HttpClient httpClient, HttpRequestMessage message)
+ {
+ try
+ {
+ return await httpClient.SendAsync(message);
+ }
+ catch (HttpRequestException exception)
+ {
+ _logger.Warning("Failed to send request to API '{ApiUrl:l}': {Message:l}", message.RequestUri, exception.Message);
return null;
}
}
+
+ private static HttpRequestMessage CreateHttpRequestMessage(string json, string url, string auth)
+ {
+ var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
+ var message = new HttpRequestMessage(HttpMethod.Post, url)
+ {
+ Content = jsonContent
+ };
+ message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth);
+ return message;
+ }
}
public class MultiFactorApiResponse
diff --git a/MultiFactor.Ldap.Adapter/packages.config b/MultiFactor.Ldap.Adapter/packages.config
index f15a554..31150d1 100644
--- a/MultiFactor.Ldap.Adapter/packages.config
+++ b/MultiFactor.Ldap.Adapter/packages.config
@@ -7,14 +7,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
@@ -25,7 +48,8 @@
-
+
+
@@ -33,6 +57,7 @@
+
\ No newline at end of file