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