From 576759523e39693e833de8ccea9397db06dba1c1 Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Tue, 18 Feb 2025 16:16:57 +0500 Subject: [PATCH 1/8] DEV-344 Add retries to the Multifactor API with fallbacks --- src/Configuration/ServiceConfiguration.cs | 14 ++-- src/MultiFactor.Ldap.Adapter.csproj | 1 + src/Services/MultiFactorApiClient.cs | 95 +++++++++++++++++++---- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/Configuration/ServiceConfiguration.cs b/src/Configuration/ServiceConfiguration.cs index 8b218b6..d95adb3 100644 --- a/src/Configuration/ServiceConfiguration.cs +++ b/src/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.Configuration.Core; @@ -64,7 +64,7 @@ public ClientConfiguration GetClient(IPAddress ip) /// /// Multifactor API URL /// - public string ApiUrl { get; set; } + public string[] ApiUrls { get; set; } /// /// HTTP Proxy for API @@ -90,7 +90,7 @@ public ClientConfiguration GetClient(IPAddress ip) /// Certificate for TLS /// public X509Certificate2 X509Certificate { get; set; } - + /// /// Certificate Password /// @@ -127,7 +127,7 @@ private void Load() throw new Exception("Configuration error: 'logging-level' element not found"); } - ApiUrl = apiUrlSetting; + ApiUrls = apiUrlSetting.Split(';').ToArray(); ApiTimeout = apiTimeout; ApiProxy = apiProxySetting; LogLevel = logLevelSetting; @@ -137,7 +137,7 @@ private void Load() { throw new Exception("Configuration error: Neither 'adapter-ldap-endpoint' or 'adapter-ldaps-endpoint' configured"); } - + ServerConfig = ldapServerConfig; if (!string.IsNullOrEmpty(certificatePassword)) @@ -230,12 +230,12 @@ private static ClientConfiguration Load(string name, AppSettingsSection appSetti } LdapIdentityFormat transformLdapIdentityFormat = LdapIdentityFormat.None; - if (!string.IsNullOrEmpty(transformLdapIdentityString) && + if (!string.IsNullOrEmpty(transformLdapIdentityString) && !Enum.TryParse(transformLdapIdentityString, true, out transformLdapIdentityFormat)) { throw new Exception("Configuration error: 'transform-ldap-identity' element has a wrong value"); } - + var configuration = new ClientConfiguration { Name = name, diff --git a/src/MultiFactor.Ldap.Adapter.csproj b/src/MultiFactor.Ldap.Adapter.csproj index 17e0c67..a7b72ed 100644 --- a/src/MultiFactor.Ldap.Adapter.csproj +++ b/src/MultiFactor.Ldap.Adapter.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index ed1c435..addc114 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -1,16 +1,19 @@ //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.Configuration; using MultiFactor.Ldap.Adapter.Services.Caching; using Serilog; using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Polly; +using Polly.Wrap; namespace MultiFactor.Ldap.Adapter.Services { @@ -63,13 +66,13 @@ public async Task Authenticate(ConnectedClientInfo connectedClient) return true; } - var url = _configuration.ApiUrl + "/access/requests/la"; + var urls = _configuration.ApiUrls.Select(url => url + "/access/requests/la").ToArray(); var payload = new { Identity = connectedClient.Username, }; - var response = await SendRequest(connectedClient.ClientConfiguration, url, payload); + var response = await SendRequest(connectedClient.ClientConfiguration, urls, payload); if (response == null) { @@ -78,7 +81,7 @@ public async Task Authenticate(ConnectedClientInfo connectedClient) if (response.Granted && !response.Bypassed) { - _logger.Information("Second factor for user '{user:l}' verified successfully. Authenticator '{authenticator:l}', account '{account:l}'", + _logger.Information("Second factor for user '{user:l}' verified successfully. Authenticator '{authenticator:l}', account '{account:l}'", connectedClient.Username, response?.Authenticator, response?.Account); _clientCache.SetCache(connectedClient.Username, connectedClient.ClientConfiguration); } @@ -87,14 +90,14 @@ 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[] urls, object payload) { try { @@ -110,18 +113,23 @@ private async Task SendRequest(ClientConfiguration cli 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) + var fallbackUrls = urls.Skip(1).ToArray(); + var retryStrategy = ConfigureRetryStrategy(httpClient, json, auth, fallbackUrls); + var res = await retryStrategy.ExecuteAsync(async () => { - Content = jsonContent - }; - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); - var res = await httpClient.SendAsync(message); - + var jsonContent = new StringContent(json, Encoding.UTF8, "application/json"); + var message = new HttpRequestMessage(HttpMethod.Post, urls[0]) + { + Content = jsonContent + }; + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); + return await httpClient.SendAsync(message); + }); + if (res.StatusCode == HttpStatusCode.TooManyRequests) { _logger.Warning("Got unsuccessful response from API: {@response}", res.ReasonPhrase); - return new MultiFactorAccessRequest() { Status = "Denied", ReplyMessage = "Too many requests"}; + return new MultiFactorAccessRequest() { Status = "Denied", ReplyMessage = "Too many requests" }; } var jsonResponse = await res.Content.ReadAsStringAsync(); @@ -138,7 +146,7 @@ private async Task SendRequest(ClientConfiguration cli } catch (TaskCanceledException tce) { - _logger.Error(tce, $"Multifactor API host unreachable {url}: timeout"); + _logger.Error(tce, $"Multifactor API host unreachable {string.Join(';', urls)}: timeout"); if (clientConfig.BypassSecondFactorWhenApiUnreachable) { @@ -150,7 +158,7 @@ private async Task SendRequest(ClientConfiguration cli } catch (Exception ex) { - _logger.Error(ex, $"Multifactor API host unreachable {url}: {ex.Message}"); + _logger.Error(ex, $"Multifactor API host unreachable {string.Join(';', urls)}: {ex.Message}"); if (clientConfig.BypassSecondFactorWhenApiUnreachable) { @@ -161,6 +169,59 @@ private async Task SendRequest(ClientConfiguration cli return null; } } + + private AsyncPolicyWrap ConfigureRetryStrategy( + HttpClient httpClient, + string json, + string auth, + string[] fallbackUrls, + int maxRetries = 2, + int timoutSeconds = 3) + { + var timeoutPolicy = Policy + .TimeoutAsync(timoutSeconds, + (context, timeSpan, task) => + { + _logger.Warning($"The request to the main Multifactor API was timed out: {timeSpan.Seconds} seconds."); + return Task.CompletedTask; + }); + + var retryPolicy = Policy + .Handle() + .WaitAndRetryAsync(maxRetries, + attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), + (exception, timeSpan, attempt, context) => + { + _logger.Warning($"The request to the main Multifactor API failed. Attempt number is {attempt}/{maxRetries}. Retrying in {timeSpan.Seconds} seconds..."); + }); + + var fallbackPolicy = Policy + .Handle() + .FallbackAsync(async cancellationToken => + { + foreach (var fallbackUrl in fallbackUrls) + { + try + { + var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");; + var message = new HttpRequestMessage(HttpMethod.Post, fallbackUrl) { Content = jsonContent }; + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); + _logger.Warning($"The main Multifactor API is unreachable. Trying to fallback: {fallbackUrl}..."); + return await httpClient.SendAsync(message, cancellationToken); + } + catch (HttpRequestException) + { + _logger.Warning($"Fallback URL {fallbackUrl} is unreachable."); + } + } + + throw new HttpRequestException("All the Multifactor APIs is unreachable."); + }); + + return fallbackPolicy + .WrapAsync(retryPolicy) + .WrapAsync(timeoutPolicy); + } } public class MultiFactorApiResponse @@ -190,6 +251,6 @@ public static MultiFactorAccessRequest Bypass { return new MultiFactorAccessRequest { Status = "Granted", Bypassed = true }; } - } + } } } From f2d558549aac5192056e3435c971f3ad0cda4034 Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Thu, 20 Feb 2025 13:15:33 +0500 Subject: [PATCH 2/8] DEV-344 Change retry strategy mechanism --- src/MultiFactor.Ldap.Adapter.csproj | 1 + src/Program.cs | 35 +++++++++- src/Services/MultiFactorApiClient.cs | 101 ++++++++------------------- 3 files changed, 64 insertions(+), 73 deletions(-) diff --git a/src/MultiFactor.Ldap.Adapter.csproj b/src/MultiFactor.Ldap.Adapter.csproj index a7b72ed..2394d93 100644 --- a/src/MultiFactor.Ldap.Adapter.csproj +++ b/src/MultiFactor.Ldap.Adapter.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Program.cs b/src/Program.cs index 41d2d3d..d0a87cc 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,4 +1,3 @@ -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MultiFactor.Ldap.Adapter.Configuration; @@ -12,7 +11,11 @@ using Serilog; using Serilog.Core; using System; +using System.Net.Http; using System.Text; +using System.Threading.Tasks; +using Polly; +using Polly.Wrap; namespace MultiFactor.Ldap.Adapter { @@ -68,6 +71,12 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(prov => new RandomWaiter(prov.GetRequiredService().InvalidCredentialDelay)); services.AddSingleton(); services.AddHttpClientWithProxy(); + services.AddHttpClient(nameof(MultiFactorApiClient)) + .AddPolicyHandler((provider, request) => + { + var logger = provider.GetRequiredService(); + return ConfigureRetryStrategy(logger); + }); services.AddSingleton(); services.AddSingleton(); services.AddMemoryCache(); @@ -99,5 +108,29 @@ private static string FlattenException(Exception exception) return stringBuilder.ToString(); } + + private static AsyncPolicyWrap ConfigureRetryStrategy(ILogger logger) + { + const int maxRetries = 2; + const int timeoutSeconds = 3; + var timeoutPolicy = Policy + .TimeoutAsync(timeoutSeconds, + (context, timeSpan, task) => + { + logger.Warning($"The request to the main Multifactor API was timed out: {timeSpan.Seconds} seconds."); + return Task.CompletedTask; + }); + + var retryPolicy = Policy + .Handle() + .WaitAndRetryAsync(maxRetries, + attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), + (exception, timeSpan, attempt, context) => + { + logger.Warning($"The request to the Multifactor API failed: {exception.Message}. Attempt number is {attempt}/{maxRetries}. Retrying in {timeSpan.Seconds} seconds..."); + }); + + return retryPolicy.WrapAsync(timeoutPolicy); + } } } diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index addc114..e644712 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -112,37 +112,47 @@ private async Task SendRequest(ClientConfiguration cli //basic authorization var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(clientConfig.MultifactorApiKey + ":" + clientConfig.MultifactorApiSecret)); var httpClient = _httpClientFactory.CreateClient(nameof(MultiFactorApiClient)); - - var fallbackUrls = urls.Skip(1).ToArray(); - var retryStrategy = ConfigureRetryStrategy(httpClient, json, auth, fallbackUrls); - var res = await retryStrategy.ExecuteAsync(async () => + foreach (var url in urls) { var jsonContent = new StringContent(json, Encoding.UTF8, "application/json"); - var message = new HttpRequestMessage(HttpMethod.Post, urls[0]) + var message = new HttpRequestMessage(HttpMethod.Post, url) { Content = jsonContent }; message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); - return await httpClient.SendAsync(message); - }); + HttpResponseMessage res; + try + { + res = await httpClient.SendAsync(message); + } + catch (HttpRequestException exception) + { + _logger.Warning($"Failed to send request to API '{url}': {exception.Message}"); + continue; + } - if (res.StatusCode == HttpStatusCode.TooManyRequests) - { - _logger.Warning("Got unsuccessful response from API: {@response}", res.ReasonPhrase); - return new MultiFactorAccessRequest() { Status = "Denied", ReplyMessage = "Too many requests" }; - } + if (res.StatusCode == HttpStatusCode.TooManyRequests) + { + _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 jsonResponse = await res.Content.ReadAsStringAsync(); + var response = + JsonSerializer.Deserialize>(jsonResponse, + _serialazerOptions); - _logger.Debug("Received response from API: {@response}", response); + _logger.Debug("Received response from API: {@response}", response); - if (!response.Success) - { - _logger.Warning("Got unsuccessful response from API: {@response}", response); + if (!response.Success) + { + _logger.Warning("Got unsuccessful response from API: {@response}", response); + } + + return response.Model; } - return response.Model; + throw new TaskCanceledException("Failed to send request to API."); } catch (TaskCanceledException tce) { @@ -169,59 +179,6 @@ private async Task SendRequest(ClientConfiguration cli return null; } } - - private AsyncPolicyWrap ConfigureRetryStrategy( - HttpClient httpClient, - string json, - string auth, - string[] fallbackUrls, - int maxRetries = 2, - int timoutSeconds = 3) - { - var timeoutPolicy = Policy - .TimeoutAsync(timoutSeconds, - (context, timeSpan, task) => - { - _logger.Warning($"The request to the main Multifactor API was timed out: {timeSpan.Seconds} seconds."); - return Task.CompletedTask; - }); - - var retryPolicy = Policy - .Handle() - .WaitAndRetryAsync(maxRetries, - attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), - (exception, timeSpan, attempt, context) => - { - _logger.Warning($"The request to the main Multifactor API failed. Attempt number is {attempt}/{maxRetries}. Retrying in {timeSpan.Seconds} seconds..."); - }); - - var fallbackPolicy = Policy - .Handle() - .FallbackAsync(async cancellationToken => - { - foreach (var fallbackUrl in fallbackUrls) - { - try - { - var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");; - var message = new HttpRequestMessage(HttpMethod.Post, fallbackUrl) { Content = jsonContent }; - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); - _logger.Warning($"The main Multifactor API is unreachable. Trying to fallback: {fallbackUrl}..."); - return await httpClient.SendAsync(message, cancellationToken); - } - catch (HttpRequestException) - { - _logger.Warning($"Fallback URL {fallbackUrl} is unreachable."); - } - } - - throw new HttpRequestException("All the Multifactor APIs is unreachable."); - }); - - return fallbackPolicy - .WrapAsync(retryPolicy) - .WrapAsync(timeoutPolicy); - } } public class MultiFactorApiResponse From 2315c77ac371757c9d668123f756cdccc722eb4d Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Mon, 24 Feb 2025 14:20:08 +0500 Subject: [PATCH 3/8] DEV-344 Fix trace-id --- src/Services/MfTraceIdHeaderSetter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/MfTraceIdHeaderSetter.cs b/src/Services/MfTraceIdHeaderSetter.cs index 26aebc8..dfc917c 100644 --- a/src/Services/MfTraceIdHeaderSetter.cs +++ b/src/Services/MfTraceIdHeaderSetter.cs @@ -25,7 +25,7 @@ protected override async Task SendAsync(HttpRequestMessage } else { - request.Headers.Add(_key, $"ldl-{Guid.NewGuid()}"); + request.Headers.Add(_key, $"ldp-{Guid.NewGuid()}"); } var resp = await base.SendAsync(request, cancellationToken); From 2f74094bcf59bbf7b54b0f6fd4a05aadffa731eb Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Mon, 24 Feb 2025 15:32:35 +0500 Subject: [PATCH 4/8] DEV-344 Rewrite to resilience lib --- .../ServiceCollectionExtensions.cs | 13 ++- src/MultiFactor.Ldap.Adapter.csproj | 3 +- src/Program.cs | 34 ------ src/Services/MultiFactorApiClient.cs | 106 +++++++++--------- 4 files changed, 64 insertions(+), 92 deletions(-) diff --git a/src/Configuration/ServiceCollectionExtensions.cs b/src/Configuration/ServiceCollectionExtensions.cs index 7301513..715002b 100644 --- a/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/Configuration/ServiceCollectionExtensions.cs @@ -3,6 +3,8 @@ using Serilog; using System; using System.Net.Http; +using Microsoft.Extensions.Http.Resilience; +using Polly; namespace MultiFactor.Ldap.Adapter.Configuration { @@ -39,7 +41,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 + }); + }); } } } diff --git a/src/MultiFactor.Ldap.Adapter.csproj b/src/MultiFactor.Ldap.Adapter.csproj index 2394d93..1e8090c 100644 --- a/src/MultiFactor.Ldap.Adapter.csproj +++ b/src/MultiFactor.Ldap.Adapter.csproj @@ -12,9 +12,8 @@ - + - diff --git a/src/Program.cs b/src/Program.cs index d0a87cc..d9020ae 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -11,11 +11,7 @@ using Serilog; using Serilog.Core; using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -using Polly; -using Polly.Wrap; namespace MultiFactor.Ldap.Adapter { @@ -71,12 +67,6 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(prov => new RandomWaiter(prov.GetRequiredService().InvalidCredentialDelay)); services.AddSingleton(); services.AddHttpClientWithProxy(); - services.AddHttpClient(nameof(MultiFactorApiClient)) - .AddPolicyHandler((provider, request) => - { - var logger = provider.GetRequiredService(); - return ConfigureRetryStrategy(logger); - }); services.AddSingleton(); services.AddSingleton(); services.AddMemoryCache(); @@ -108,29 +98,5 @@ private static string FlattenException(Exception exception) return stringBuilder.ToString(); } - - private static AsyncPolicyWrap ConfigureRetryStrategy(ILogger logger) - { - const int maxRetries = 2; - const int timeoutSeconds = 3; - var timeoutPolicy = Policy - .TimeoutAsync(timeoutSeconds, - (context, timeSpan, task) => - { - logger.Warning($"The request to the main Multifactor API was timed out: {timeSpan.Seconds} seconds."); - return Task.CompletedTask; - }); - - var retryPolicy = Policy - .Handle() - .WaitAndRetryAsync(maxRetries, - attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), - (exception, timeSpan, attempt, context) => - { - logger.Warning($"The request to the Multifactor API failed: {exception.Message}. Attempt number is {attempt}/{maxRetries}. Retrying in {timeSpan.Seconds} seconds..."); - }); - - return retryPolicy.WrapAsync(timeoutPolicy); - } } } diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index e644712..f75221d 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -12,8 +12,6 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; -using Polly; -using Polly.Wrap; namespace MultiFactor.Ldap.Adapter.Services { @@ -66,13 +64,12 @@ public async Task Authenticate(ConnectedClientInfo connectedClient) return true; } - var urls = _configuration.ApiUrls.Select(url => url + "/access/requests/la").ToArray(); var payload = new { Identity = connectedClient.Username, }; - var response = await SendRequest(connectedClient.ClientConfiguration, urls, payload); + var response = await SendRequest(connectedClient.ClientConfiguration, _configuration.ApiUrls, payload); if (response == null) { @@ -97,50 +94,37 @@ public async Task Authenticate(ConnectedClientInfo connectedClient) return response.Granted; } - private async Task SendRequest(ClientConfiguration clientConfig, string[] urls, 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: {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)); - foreach (var url in urls) + foreach (var url in baseUrls.Select(baseUrl => $"{baseUrl}/access/requests/la")) + { + try { - 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); - HttpResponseMessage res; - try - { - res = await httpClient.SendAsync(message); - } - catch (HttpRequestException exception) - { - _logger.Warning($"Failed to send request to API '{url}': {exception.Message}"); + var message = PrepareHttpRequestMessage(json, url, auth); + var res = await TrySendRequest(httpClient, message); + if (res == null) continue; - } if (res.StatusCode == HttpStatusCode.TooManyRequests) { _logger.Warning("Got unsuccessful response from API: {@response}", res.ReasonPhrase); - return new MultiFactorAccessRequest() { Status = "Denied", ReplyMessage = "Too many requests" }; + return new MultiFactorAccessRequest { Status = "Denied", ReplyMessage = "Too many requests" }; } var jsonResponse = await res.Content.ReadAsStringAsync(); - var response = - JsonSerializer.Deserialize>(jsonResponse, - _serialazerOptions); + var response = JsonSerializer.Deserialize>(jsonResponse, _serialazerOptions); _logger.Debug("Received response from API: {@response}", response); @@ -150,35 +134,47 @@ private async Task SendRequest(ClientConfiguration cli } return response.Model; - } - - throw new TaskCanceledException("Failed to send request to API."); - } - catch (TaskCanceledException tce) - { - _logger.Error(tce, $"Multifactor API host unreachable {string.Join(';', urls)}: timeout"); - if (clientConfig.BypassSecondFactorWhenApiUnreachable) + } + catch (Exception ex) { - _logger.Warning("Bypass second factor"); - return MultiFactorAccessRequest.Bypass; + _logger.Error(ex, $"Multifactor API host unreachable {url}: {ex.Message}."); } - - return null; } - catch (Exception ex) - { - _logger.Error(ex, $"Multifactor API host unreachable {string.Join(';', urls)}: {ex.Message}"); - if (clientConfig.BypassSecondFactorWhenApiUnreachable) - { - _logger.Warning("Bypass second factor"); - return MultiFactorAccessRequest.Bypass; - } + _logger.Error($"Multifactor API host unreachable {string.Join(';', baseUrls)}: replicas exhausted."); + + if (clientConfig.BypassSecondFactorWhenApiUnreachable) + { + _logger.Warning("Bypass second factor"); + return MultiFactorAccessRequest.Bypass; + } + return null; + } + private async Task TrySendRequest(HttpClient httpClient, HttpRequestMessage message) + { + try + { + return await httpClient.SendAsync(message); + } + catch (HttpRequestException exception) + { + _logger.Warning($"Failed to send request to API '{message.RequestUri}': {exception.Message}"); return null; } } + + private static HttpRequestMessage PrepareHttpRequestMessage(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 From 2aacdcce9a057d9ab3719784a415d447827d410d Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Mon, 24 Feb 2025 15:42:48 +0500 Subject: [PATCH 5/8] DEV-344 Fix ApiUrls setting reading --- src/Configuration/ServiceConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Configuration/ServiceConfiguration.cs b/src/Configuration/ServiceConfiguration.cs index d95adb3..53c7b62 100644 --- a/src/Configuration/ServiceConfiguration.cs +++ b/src/Configuration/ServiceConfiguration.cs @@ -127,7 +127,7 @@ private void Load() throw new Exception("Configuration error: 'logging-level' element not found"); } - ApiUrls = apiUrlSetting.Split(';').ToArray(); + ApiUrls = apiUrlSetting.Split(';', StringSplitOptions.RemoveEmptyEntries); ApiTimeout = apiTimeout; ApiProxy = apiProxySetting; LogLevel = logLevelSetting; From 340ee08c1d17eb40c3bca006aac52b7a9d1040bd Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Tue, 25 Feb 2025 17:38:31 +0500 Subject: [PATCH 6/8] DEV-344 Fix settings and logs --- src/Configuration/ServiceConfiguration.cs | 2 +- src/Services/MultiFactorApiClient.cs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Configuration/ServiceConfiguration.cs b/src/Configuration/ServiceConfiguration.cs index 53c7b62..856bac3 100644 --- a/src/Configuration/ServiceConfiguration.cs +++ b/src/Configuration/ServiceConfiguration.cs @@ -127,7 +127,7 @@ private void Load() throw new Exception("Configuration error: 'logging-level' element not found"); } - ApiUrls = apiUrlSetting.Split(';', StringSplitOptions.RemoveEmptyEntries); + ApiUrls = apiUrlSetting.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Distinct().ToArray(); ApiTimeout = apiTimeout; ApiProxy = apiProxySetting; LogLevel = logLevelSetting; diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index f75221d..f42c0c8 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -102,14 +102,15 @@ private async Task SendRequest(ClientConfiguration cli 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 auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientConfig.MultifactorApiKey}:{clientConfig.MultifactorApiSecret}")); var httpClient = _httpClientFactory.CreateClient(nameof(MultiFactorApiClient)); foreach (var url in baseUrls.Select(baseUrl => $"{baseUrl}/access/requests/la")) { + _logger.Information("Sending request to API '{ApiUrl:l}'.", url); try { var message = PrepareHttpRequestMessage(json, url, auth); @@ -119,14 +120,14 @@ private async Task SendRequest(ClientConfiguration cli if (res.StatusCode == HttpStatusCode.TooManyRequests) { - _logger.Warning("Got unsuccessful response from API: {@response}", res.ReasonPhrase); + _logger.Warning("Got unsuccessful response from API '{ApiUrl:l}': {@response}", url, res.ReasonPhrase); return new MultiFactorAccessRequest { Status = "Denied", ReplyMessage = "Too many requests" }; } var jsonResponse = await res.Content.ReadAsStringAsync(); var response = JsonSerializer.Deserialize>(jsonResponse, _serialazerOptions); - _logger.Debug("Received response from API: {@response}", response); + _logger.Debug("Received response from API '{ApiUrl:l}': {@response}", url, response); if (!response.Success) { @@ -138,11 +139,11 @@ private async Task SendRequest(ClientConfiguration cli } catch (Exception ex) { - _logger.Error(ex, $"Multifactor API host unreachable {url}: {ex.Message}."); + _logger.Error(ex, "Multifactor API host '{ApiUrl:l}' unreachable: {Message:l}", url, ex.Message); } } - _logger.Error($"Multifactor API host unreachable {string.Join(';', baseUrls)}: replicas exhausted."); + _logger.Error("Multifactor API host unreachable {ApiUrls:l}: replicas exhausted.", string.Join(';', baseUrls)); if (clientConfig.BypassSecondFactorWhenApiUnreachable) { @@ -160,7 +161,7 @@ private async Task TrySendRequest(HttpClient httpClient, Ht } catch (HttpRequestException exception) { - _logger.Warning($"Failed to send request to API '{message.RequestUri}': {exception.Message}"); + _logger.Warning("Failed to send request to API '{ApiUrl:l}': {Message:l}", message.RequestUri, exception.Message); return null; } } From 6832c8d6a317457059e28521a75c3dfada431fef Mon Sep 17 00:00:00 2001 From: Lamanov Maksim Date: Wed, 26 Feb 2025 12:44:52 +0500 Subject: [PATCH 7/8] DEV-344 Fix log --- src/Services/MultiFactorApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index f42c0c8..6484b10 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -143,7 +143,7 @@ private async Task SendRequest(ClientConfiguration cli } } - _logger.Error("Multifactor API host unreachable {ApiUrls:l}: replicas exhausted.", string.Join(';', baseUrls)); + _logger.Error("Multifactor API Cloud unreachable"); if (clientConfig.BypassSecondFactorWhenApiUnreachable) { From 7c5c1f56863e6f7421c1f049b877138f1e9be8d0 Mon Sep 17 00:00:00 2001 From: lamanoff Date: Thu, 3 Apr 2025 13:31:34 +0500 Subject: [PATCH 8/8] DEV-344 Fix non-failed status codes --- src/Services/MultiFactorApiClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/MultiFactorApiClient.cs b/src/Services/MultiFactorApiClient.cs index 6484b10..f4fef34 100644 --- a/src/Services/MultiFactorApiClient.cs +++ b/src/Services/MultiFactorApiClient.cs @@ -132,6 +132,7 @@ private async Task SendRequest(ClientConfiguration cli 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 response.Model;