From adde47e3c0b4435af813548aa3619a864b4939f7 Mon Sep 17 00:00:00 2001 From: Simon Abler Date: Thu, 27 Mar 2025 14:29:34 +0100 Subject: [PATCH 1/2] feat: Allow custom RestClientOptions injection for DigestAuthenticator This commit enables injecting custom RestClientOptions into DigestAuthenticator, allowing full control over the internal RestClient used during the digest handshake. It provides flexibility to configure SSL validation, proxies, and other client-specific settings without creating authentication loops. - Added optional RestClientOptions parameter to DigestAuthenticator - Passed RestClientOptions through DigestAuthenticatorManager - Ensured backward compatibility (default behavior remains unchanged)" --- RELEASES.md | 6 ++++- .../DigestAuthenticator.cs | 8 ++++--- .../DigestAuthenticatorManager.cs | 22 ++++++++++++++----- .../DigestIntegrationTest.cs | 22 +++++++++++++++++++ .../Fixtures/DigestServerStub.cs | 15 +++++++++++-- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 49d72b2..166598f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -18,4 +18,8 @@ ## v2.0.0 -- Changes the compatibility to the RestSharp version `111.2.0` or greater. \ No newline at end of file +- Changes the compatibility to the RestSharp version `111.2.0` or greater. + +## v2.0.1 + +- Add ability to change client options from digest RestClient object \ No newline at end of file diff --git a/src/DigestAuthenticator/DigestAuthenticator.cs b/src/DigestAuthenticator/DigestAuthenticator.cs index 90a4e9e..c1ec638 100644 --- a/src/DigestAuthenticator/DigestAuthenticator.cs +++ b/src/DigestAuthenticator/DigestAuthenticator.cs @@ -16,6 +16,7 @@ public class DigestAuthenticator : IAuthenticator private readonly string _username; private readonly TimeSpan _timeout; + private readonly RestClientOptions? _handshakeClientOptions; /// /// Creates a new instance of class. @@ -24,7 +25,7 @@ public class DigestAuthenticator : IAuthenticator /// The password. /// The optional logger. /// The request timeout. - public DigestAuthenticator(string username, string password, int timeout = DEFAULT_TIMEOUT, ILogger? logger = null) + public DigestAuthenticator(string username, string password, int timeout = DEFAULT_TIMEOUT, ILogger? logger = null, RestClientOptions? restClientOptions=null) { if (string.IsNullOrWhiteSpace(username)) { @@ -45,6 +46,7 @@ public DigestAuthenticator(string username, string password, int timeout = DEFAU _password = password; _timeout = TimeSpan.FromMilliseconds(timeout); _logger = logger ?? NullLogger.Instance; + _handshakeClientOptions = restClientOptions; } /// @@ -52,8 +54,8 @@ public async ValueTask Authenticate(IRestClient client, RestRequest request) { _logger.LogDebug("Initiate Digest authentication"); var uri = client.BuildUri(request); - var manager = new DigestAuthenticatorManager(client.BuildUri(new RestRequest()), _username, _password, _timeout, _logger); - await manager.GetDigestAuthHeader(uri.PathAndQuery, request.Method,client.Options.Proxy).ConfigureAwait(false); + var manager = new DigestAuthenticatorManager(client.BuildUri(new RestRequest()), _username, _password, _timeout, _handshakeClientOptions, _logger); + await manager.GetDigestAuthHeader(uri.PathAndQuery, request.Method, client.Options.Proxy).ConfigureAwait(false); var digestHeader = manager.GetDigestHeader(uri.PathAndQuery, request.Method); request.AddOrUpdateHeader("Connection", "Keep-Alive"); request.AddOrUpdateHeader(KnownHeaders.Authorization, digestHeader); diff --git a/src/DigestAuthenticator/DigestAuthenticatorManager.cs b/src/DigestAuthenticator/DigestAuthenticatorManager.cs index 26f39fd..e66bc92 100644 --- a/src/DigestAuthenticator/DigestAuthenticatorManager.cs +++ b/src/DigestAuthenticator/DigestAuthenticatorManager.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Net.Security; using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography; @@ -47,6 +48,8 @@ internal class DigestAuthenticatorManager /// private string? _realm; + private readonly RestClientOptions? _handshakeClientOptions; + static DigestAuthenticatorManager() { _assemblyVersion = Assembly.GetAssembly(typeof(DigestAuthenticatorManager)).GetName().Version; @@ -60,7 +63,7 @@ static DigestAuthenticatorManager() /// The password. /// The timeout. /// - public DigestAuthenticatorManager(Uri host, string username, string password, TimeSpan timeout, ILogger logger) + public DigestAuthenticatorManager(Uri host, string username, string password, TimeSpan timeout, RestClientOptions? handshakeClientOptions, ILogger logger) { if (string.IsNullOrWhiteSpace(username)) { @@ -82,6 +85,7 @@ public DigestAuthenticatorManager(Uri host, string username, string password, Ti _password = password; _timeout = timeout; _logger = logger; + _handshakeClientOptions = handshakeClientOptions; } /// @@ -93,7 +97,7 @@ public DigestAuthenticatorManager(Uri host, string username, string password, Ti public async Task GetDigestAuthHeader( string path, Method method, - IWebProxy? proxy = default) + IWebProxy? proxy = null) { _logger.LogDebug("Initiating GetDigestAuthHeader"); var uri = new Uri(_host, path); @@ -103,11 +107,19 @@ public async Task GetDigestAuthHeader( request.AddOrUpdateHeader("User-Agent", $"RestSharp.Authenticators.Digest/{_assemblyVersion}"); request.AddOrUpdateHeader("Accept-Encoding", "gzip, deflate, br"); request.Timeout = _timeout; - using var client = new RestClient(new RestClientOptions() + + RestClient client; + if (_handshakeClientOptions != null) + { + client = new RestClient(_handshakeClientOptions); + } + else { - Proxy = proxy - }); + client = new RestClient(new RestClientOptions() { Proxy = proxy }); + } + var response = await client.ExecuteAsync(request).ConfigureAwait(false); + client.Dispose(); GetDigestDataFromFailResponse(response); _logger.LogDebug("GetDigestAuthHeader completed"); } diff --git a/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs b/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs index d4470b9..b1155e5 100644 --- a/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs +++ b/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs @@ -37,4 +37,26 @@ public async Task Given_ADigestAuthEndpoint_When_ITryToGetInfo_Then_TheAuthMustB response.StatusCode.Should().Be(HttpStatusCode.OK); loggerMock.ReceivedWithAnyArgs().LogDebug("NONONO"); } + + + [Fact] + public async Task Given_ADigestAuthEndpoint_When_ITryToInjectOwnClient_Then_TheAuthMustBeResolved() + { + var loggerMock = Substitute.For(); + loggerMock.BeginScope("DigestServerStub"); + + var request = new RestRequest("values"); + request.AddHeader("Content-Type", "application/json"); + + RestClientOptions options = new RestClientOptions() + { + MaxRedirects = 2 + }; + + var client = _fixture.CreateInjectedOptionClient(loggerMock, options); + var response = await client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + loggerMock.ReceivedWithAnyArgs().LogDebug("NONONO"); + } } diff --git a/test/DigestAuthenticator.Tests/Fixtures/DigestServerStub.cs b/test/DigestAuthenticator.Tests/Fixtures/DigestServerStub.cs index fbd207d..1ebb73f 100644 --- a/test/DigestAuthenticator.Tests/Fixtures/DigestServerStub.cs +++ b/test/DigestAuthenticator.Tests/Fixtures/DigestServerStub.cs @@ -15,7 +15,7 @@ public class DigestServerStub : IAsyncDisposable { private readonly CancellationTokenSource _cancellationTokenSource; private readonly Task _serverTask; - + private const string REALM = "test-realm"; private const string USERNAME = "test-user"; private const string PASSWORD = "test-password"; @@ -26,7 +26,7 @@ public DigestServerStub() var nonce = GenerateNonce(); _cancellationTokenSource = new CancellationTokenSource(); - + _serverTask = StartServer(REALM, USERNAME, PASSWORD, nonce, PORT); Console.WriteLine($"Server started! port: {PORT}."); } @@ -41,6 +41,17 @@ public IRestClient CreateClient(ILogger logger) return new RestClient(restOptions); } + public IRestClient CreateInjectedOptionClient(ILogger logger, RestClientOptions clientOptions ) + { + + var restOptions = new RestClientOptions($"http://localhost:{PORT}") + { + Authenticator = new DigestAuthenticator(USERNAME, PASSWORD, logger: logger, restClientOptions: clientOptions) + }; + + return new RestClient(restOptions); + } + public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); From e415733c9aaaba165177c25e76d577ebe3e2efdd Mon Sep 17 00:00:00 2001 From: Simon Abler Date: Thu, 27 Mar 2025 14:39:38 +0100 Subject: [PATCH 2/2] test: Verify DigestAuthenticator uses injected RestClientOptions Added unit test VerifyHandshakeUsesInjectedClientOptions to ensure DigestAuthenticator correctly applies injected RestClientOptions (tested via Proxy callback) during the digest handshake request. --- .../DigestIntegrationTest.cs | 7 ++++- .../Fixtures/TestProxy.cs | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/DigestAuthenticator.Tests/Fixtures/TestProxy.cs diff --git a/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs b/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs index b1155e5..582c8aa 100644 --- a/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs +++ b/test/DigestAuthenticator.Tests/DigestIntegrationTest.cs @@ -42,6 +42,8 @@ public async Task Given_ADigestAuthEndpoint_When_ITryToGetInfo_Then_TheAuthMustB [Fact] public async Task Given_ADigestAuthEndpoint_When_ITryToInjectOwnClient_Then_TheAuthMustBeResolved() { + bool proxyCalled = false; + var loggerMock = Substitute.For(); loggerMock.BeginScope("DigestServerStub"); @@ -50,7 +52,7 @@ public async Task Given_ADigestAuthEndpoint_When_ITryToInjectOwnClient_Then_TheA RestClientOptions options = new RestClientOptions() { - MaxRedirects = 2 + Proxy = new TestProxy(() => proxyCalled = true) }; var client = _fixture.CreateInjectedOptionClient(loggerMock, options); @@ -58,5 +60,8 @@ public async Task Given_ADigestAuthEndpoint_When_ITryToInjectOwnClient_Then_TheA response.StatusCode.Should().Be(HttpStatusCode.OK); loggerMock.ReceivedWithAnyArgs().LogDebug("NONONO"); + + Assert.True(proxyCalled, "Injected RestClientOptions.Proxy should be used in digest handshake."); + } } diff --git a/test/DigestAuthenticator.Tests/Fixtures/TestProxy.cs b/test/DigestAuthenticator.Tests/Fixtures/TestProxy.cs new file mode 100644 index 0000000..035419c --- /dev/null +++ b/test/DigestAuthenticator.Tests/Fixtures/TestProxy.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace RestSharp.Authenticators.Digest.Tests.Fixtures; +internal class TestProxy : IWebProxy +{ + private readonly Action _onProxyCalled; + + public TestProxy(Action onProxyCalled) + { + _onProxyCalled = onProxyCalled; + } + + public Uri GetProxy(Uri destination) + { + _onProxyCalled(); + return destination; + } + + public bool IsBypassed(Uri host) => false; + + public ICredentials? Credentials { get; set; } +}