diff --git a/src/Trakx.Canton.ApiClient/Configuration/AuthenticationRequestHandler.cs b/src/Trakx.Canton.ApiClient/Auth/AuthenticationRequestHandler.cs
similarity index 100%
rename from src/Trakx.Canton.ApiClient/Configuration/AuthenticationRequestHandler.cs
rename to src/Trakx.Canton.ApiClient/Auth/AuthenticationRequestHandler.cs
diff --git a/src/Trakx.Canton.ApiClient/Configuration/CantonApiClientCredentials.cs b/src/Trakx.Canton.ApiClient/Auth/CantonApiClientCredentials.cs
similarity index 100%
rename from src/Trakx.Canton.ApiClient/Configuration/CantonApiClientCredentials.cs
rename to src/Trakx.Canton.ApiClient/Auth/CantonApiClientCredentials.cs
diff --git a/src/Trakx.Canton.ApiClient/Auth/StaticJwtCredentialsProvider.cs b/src/Trakx.Canton.ApiClient/Auth/StaticJwtCredentialsProvider.cs
new file mode 100644
index 0000000..5e50e4a
--- /dev/null
+++ b/src/Trakx.Canton.ApiClient/Auth/StaticJwtCredentialsProvider.cs
@@ -0,0 +1,50 @@
+using System.Net.Http.Headers;
+using Trakx.Authentication;
+using Trakx.Authentication.Client;
+
+namespace Trakx.Canton.ApiClient;
+
+///
+/// Provides static JWT credentials.
+///
+public class StaticJwtCredentialsProvider : IAuthCredentialsProvider
+{
+ private readonly string _authToken;
+
+ /// Constructor.
+ public StaticJwtCredentialsProvider(string authToken)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(authToken);
+ _authToken = authToken;
+ }
+
+ ///
+ public void AddCredentials(HttpRequestMessage msg)
+ {
+ var credentials = GetCredentials();
+ msg.Headers.Authorization = new AuthenticationHeaderValue(credentials.Scheme, credentials.Token);
+ }
+
+ ///
+ public Task AddCredentialsAsync(HttpRequestMessage msg, CancellationToken cancellationToken = default)
+ {
+ AddCredentials(msg);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public AuthCredentials GetCredentials()
+ {
+ return new AuthCredentials
+ {
+ Scheme = AuthenticationConstants.CredentialsScheme.Bearer,
+ Token = _authToken
+ };
+ }
+
+ ///
+ public Task GetCredentialsAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(GetCredentials());
+ }
+}
\ No newline at end of file
diff --git a/src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs b/src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs
index a392b83..670767c 100644
--- a/src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs
+++ b/src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs
@@ -15,6 +15,7 @@ public class CantonApiClientFactory : ICantonApiClientFactory
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly CantonApiClientConfiguration _configuration;
private readonly SemaphoreSlim _apiKeyAuthGate = new(1, 1);
+ private readonly SemaphoreSlim _jwtFromRequestAuthGate = new(1, 1);
private readonly SemaphoreSlim _jwtAuthGate = new(1, 1);
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30);
private readonly RefitSettings _refitSettings;
@@ -59,9 +60,9 @@ public TClient CreateClient(CantonApiClientCredentials credentials) whe
}
///
- public TClient CreateClientWithJwtAuth() where TClient : ICantonApiClientBase
+ public TClient CreateClientWithJwtAuth(string authToken) where TClient : ICantonApiClientBase
{
- var cacheKey = GetJwtAuthCacheKey();
+ var cacheKey = GetJwtAuthCacheKey(authToken);
if (_cache.TryGetValue(cacheKey, out var cachedClient))
return cachedClient!;
@@ -72,7 +73,8 @@ public TClient CreateClientWithJwtAuth() where TClient : ICantonApiClie
if (_cache.TryGetValue(cacheKey, out cachedClient))
return cachedClient!;
- var client = CreateClientWithJwtAuthInternal();
+ var credentialsProvider = new StaticJwtCredentialsProvider(authToken);
+ var client = CreateClientInternal(credentialsProvider);
_cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
return client;
}
@@ -82,6 +84,31 @@ public TClient CreateClientWithJwtAuth() where TClient : ICantonApiClie
}
}
+ ///
+ public TClient CreateClientWithJwtAuthFromCurrentRequest() where TClient : ICantonApiClientBase
+ {
+ var cacheKey = GetJwtAuthFromCurrentCacheKey();
+
+ if (_cache.TryGetValue(cacheKey, out var cachedClient))
+ return cachedClient!;
+
+ _jwtFromRequestAuthGate.Wait();
+ try
+ {
+ if (_cache.TryGetValue(cacheKey, out cachedClient))
+ return cachedClient!;
+
+ var credentialsProvider = new JwtCredentialsProvider(_httpContextAccessor);
+ var client = CreateClientInternal(credentialsProvider);
+ _cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
+ return client;
+ }
+ finally
+ {
+ _jwtFromRequestAuthGate.Release();
+ }
+ }
+
private TClient CreateClientWithApiKeyAuthInternal(CantonApiClientCredentials credentials)
where TClient : ICantonApiClientBase
{
@@ -95,13 +122,6 @@ private TClient CreateClientWithApiKeyAuthInternal(CantonApiClientCrede
return CreateClientInternal(credentialsProvider);
}
- private TClient CreateClientWithJwtAuthInternal()
- where TClient : ICantonApiClientBase
- {
- var credentialsProvider = new JwtCredentialsProvider(_httpContextAccessor);
- return CreateClientInternal(credentialsProvider);
- }
-
private TClient CreateClientInternal(IAuthCredentialsProvider credentialsProvider) where TClient : ICantonApiClientBase
{
var clientType = typeof(TClient);
@@ -120,6 +140,13 @@ private TClient CreateClientInternal(IAuthCredentialsProvider(CantonApiClientCredentials credentials)
=> $"Canton_ApiClient_ApiKeyAuth_{typeof(TClient).FullName}_{credentials.ClientId}";
- internal static string GetJwtAuthCacheKey()
- => $"Canton_ApiClient_JwtAuth_{typeof(TClient).FullName}";
+ internal static string GetJwtAuthFromCurrentCacheKey()
+ => $"Canton_ApiClient_JwtAuthFromRequest_{typeof(TClient).FullName}";
+
+ internal static string GetJwtAuthCacheKey(string authToken)
+ {
+ var tokenHash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(authToken));
+ var tokenHashString = Convert.ToHexString(tokenHash);
+ return $"Canton_ApiClient_JwtAuth_{tokenHashString}_{typeof(TClient).FullName}";
+ }
}
\ No newline at end of file
diff --git a/src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs b/src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs
index b5f73b0..1e89f22 100644
--- a/src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs
+++ b/src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs
@@ -15,10 +15,19 @@ public interface ICantonApiClientFactory
TClient CreateClient(CantonApiClientCredentials credentials) where TClient : ICantonApiClientBase;
///
- /// Creates an API client with JWT authentication.
+ /// Creates an API client that will use the given JWT Token as authentication token.
/// Each call creates a new HttpClient instance with its own handler chain.
///
/// The API client interface type.
+ /// The JWT token to use for authentication.
/// A new instance of the API client configured with JWT authentication handler.
- TClient CreateClientWithJwtAuth() where TClient : ICantonApiClientBase;
+ TClient CreateClientWithJwtAuth(string authToken) where TClient : ICantonApiClientBase;
+
+ ///
+ /// Creates an API client with JWT authentication using the JWT token from the current request.
+ /// Each call creates a new HttpClient instance with its own handler chain.
+ ///
+ /// The API client interface type.
+ /// A new instance of the API client configured with JWT authentication handler.
+ TClient CreateClientWithJwtAuthFromCurrentRequest() where TClient : ICantonApiClientBase;
}
\ No newline at end of file
diff --git a/tests/Trakx.Canton.ApiClient.Tests/Auth/StaticJwtCredentialsProviderTests.cs b/tests/Trakx.Canton.ApiClient.Tests/Auth/StaticJwtCredentialsProviderTests.cs
new file mode 100644
index 0000000..507a139
--- /dev/null
+++ b/tests/Trakx.Canton.ApiClient.Tests/Auth/StaticJwtCredentialsProviderTests.cs
@@ -0,0 +1,186 @@
+using System.Net.Http.Headers;
+using Trakx.Authentication;
+
+namespace Trakx.Canton.ApiClient.Tests.Auth;
+
+public class StaticJwtCredentialsProviderTests
+{
+ [Fact]
+ public void Constructor_with_valid_token_should_create_instance()
+ {
+ // Arrange
+ const string token = "valid-jwt-token";
+
+ // Act
+ var provider = new StaticJwtCredentialsProvider(token);
+
+ // Assert
+ provider.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Constructor_with_null_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => new StaticJwtCredentialsProvider(null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Constructor_with_empty_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => new StaticJwtCredentialsProvider("");
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Constructor_with_whitespace_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => new StaticJwtCredentialsProvider(" ");
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void GetCredentials_should_return_credentials_with_bearer_scheme()
+ {
+ // Arrange
+ const string token = "test-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+
+ // Act
+ var credentials = provider.GetCredentials();
+
+ // Assert
+ credentials.Should().NotBeNull();
+ credentials.Scheme.Should().Be(AuthenticationConstants.CredentialsScheme.Bearer);
+ credentials.Token.Should().Be(token);
+ }
+
+ [Fact]
+ public async Task GetCredentialsAsync_should_return_credentials_with_bearer_scheme()
+ {
+ // Arrange
+ const string token = "test-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+
+ // Act
+ var credentials = await provider.GetCredentialsAsync();
+
+ // Assert
+ credentials.Should().NotBeNull();
+ credentials.Scheme.Should().Be(AuthenticationConstants.CredentialsScheme.Bearer);
+ credentials.Token.Should().Be(token);
+ }
+
+ [Fact]
+ public void GetCredentials_called_multiple_times_should_return_same_token()
+ {
+ // Arrange
+ const string token = "consistent-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+
+ // Act
+ var credentials1 = provider.GetCredentials();
+ var credentials2 = provider.GetCredentials();
+
+ // Assert
+ credentials1.Token.Should().Be(token);
+ credentials2.Token.Should().Be(token);
+ credentials1.Token.Should().Be(credentials2.Token);
+ }
+
+ [Fact]
+ public void AddCredentials_should_set_authorization_header()
+ {
+ // Arrange
+ const string token = "test-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+ var request = new HttpRequestMessage();
+
+ // Act
+ provider.AddCredentials(request);
+
+ // Assert
+ request.Headers.Authorization.Should().NotBeNull();
+ request.Headers.Authorization!.Scheme.Should().Be(AuthenticationConstants.CredentialsScheme.Bearer);
+ request.Headers.Authorization.Parameter.Should().Be(token);
+ }
+
+ [Fact]
+ public async Task AddCredentialsAsync_should_set_authorization_header()
+ {
+ // Arrange
+ const string token = "test-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+ var request = new HttpRequestMessage();
+
+ // Act
+ await provider.AddCredentialsAsync(request);
+
+ // Assert
+ request.Headers.Authorization.Should().NotBeNull();
+ request.Headers.Authorization!.Scheme.Should().Be(AuthenticationConstants.CredentialsScheme.Bearer);
+ request.Headers.Authorization.Parameter.Should().Be(token);
+ }
+
+ [Fact]
+ public void AddCredentials_called_multiple_times_should_use_same_token()
+ {
+ // Arrange
+ const string token = "consistent-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+ var request1 = new HttpRequestMessage();
+ var request2 = new HttpRequestMessage();
+
+ // Act
+ provider.AddCredentials(request1);
+ provider.AddCredentials(request2);
+
+ // Assert
+ request1.Headers.Authorization!.Parameter.Should().Be(token);
+ request2.Headers.Authorization!.Parameter.Should().Be(token);
+ }
+
+ [Fact]
+ public void AddCredentials_should_replace_existing_authorization_header()
+ {
+ // Arrange
+ const string token = "new-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+ var request = new HttpRequestMessage();
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", "old-credentials");
+
+ // Act
+ provider.AddCredentials(request);
+
+ // Assert
+ request.Headers.Authorization.Should().NotBeNull();
+ request.Headers.Authorization!.Scheme.Should().Be(AuthenticationConstants.CredentialsScheme.Bearer);
+ request.Headers.Authorization.Parameter.Should().Be(token);
+ }
+
+ [Fact]
+ public async Task AddCredentialsAsync_with_cancellation_token_should_complete_successfully()
+ {
+ // Arrange
+ const string token = "test-jwt-token";
+ var provider = new StaticJwtCredentialsProvider(token);
+ var request = new HttpRequestMessage();
+ var cancellationToken = CancellationToken.None;
+
+ // Act
+ await provider.AddCredentialsAsync(request, cancellationToken);
+
+ // Assert
+ request.Headers.Authorization.Should().NotBeNull();
+ request.Headers.Authorization!.Parameter.Should().Be(token);
+ }
+}
\ No newline at end of file
diff --git a/tests/Trakx.Canton.ApiClient.Tests/Factories/CantonApiClientFactoryTests.cs b/tests/Trakx.Canton.ApiClient.Tests/Factories/CantonApiClientFactoryTests.cs
index 5935bf1..5b53ee8 100644
--- a/tests/Trakx.Canton.ApiClient.Tests/Factories/CantonApiClientFactoryTests.cs
+++ b/tests/Trakx.Canton.ApiClient.Tests/Factories/CantonApiClientFactoryTests.cs
@@ -149,10 +149,10 @@ public void CreateClient_after_cache_removal_should_create_new_instance()
}
[Fact]
- public void CreateClientWithJwtAuth_should_return_client()
+ public void CreateClientWithJwtAuthFromCurrentRequest_should_return_client()
{
// Act
- var client = _factory.CreateClientWithJwtAuth();
+ var client = _factory.CreateClientWithJwtAuthFromCurrentRequest();
// Assert
client.Should().NotBeNull();
@@ -160,7 +160,7 @@ public void CreateClientWithJwtAuth_should_return_client()
}
[Fact]
- public void CreateClientWithJwtAuth_concurrent_calls_should_return_same_cached_instance()
+ public void CreateClientWithJwtAuthFromCurrentRequest_concurrent_calls_should_return_same_cached_instance()
{
// Arrange
var clients = new System.Collections.Concurrent.ConcurrentBag();
@@ -168,7 +168,7 @@ public void CreateClientWithJwtAuth_concurrent_calls_should_return_same_cached_i
// Act - Create 10 concurrent requests
Parallel.For(0, 10, _ =>
{
- var client = _factory.CreateClientWithJwtAuth();
+ var client = _factory.CreateClientWithJwtAuthFromCurrentRequest();
clients.Add(client);
});
@@ -188,12 +188,148 @@ public void GetApiKeyAuthCacheKey_should_return_the_correct_key()
cacheKey.Should().NotContain(credentials.ClientSecret, "secret should not be in cache key for security");
}
+ [Fact]
+ public void GetJwtAuthFromCurrentCacheKey_should_return_the_correct_key()
+ {
+ var cacheKey = CantonApiClientFactory.GetJwtAuthFromCurrentCacheKey();
+ cacheKey.Should().StartWith("Canton_ApiClient_JwtAuthFromRequest_");
+ cacheKey.Should().Contain(typeof(IValidatorInternalClient).FullName);
+ }
+
[Fact]
public void GetJwtAuthCacheKey_should_return_the_correct_key()
{
- var cacheKey = CantonApiClientFactory.GetJwtAuthCacheKey();
+ const string authToken = "some-jwt-token";
+ var cacheKey = CantonApiClientFactory.GetJwtAuthCacheKey(authToken);
cacheKey.Should().StartWith("Canton_ApiClient_JwtAuth_");
+ cacheKey.Should().NotContain(authToken, "token is stored in hashed form");
cacheKey.Should().Contain(typeof(IValidatorInternalClient).FullName);
+
+ var tokenHash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(authToken));
+ var tokenHashString = Convert.ToHexString(tokenHash);
+ cacheKey.Should().Contain(tokenHashString);
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_valid_token_should_return_client()
+ {
+ // Arrange
+ const string authToken = "valid-jwt-token";
+
+ // Act
+ var client = _factory.CreateClientWithJwtAuth(authToken);
+
+ // Assert
+ client.Should().NotBeNull();
+ client.Should().BeAssignableTo();
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_null_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => _factory.CreateClientWithJwtAuth(null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_empty_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => _factory.CreateClientWithJwtAuth("");
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_whitespace_token_should_throw_ArgumentException()
+ {
+ // Act
+ var act = () => _factory.CreateClientWithJwtAuth(" ");
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_called_twice_with_same_token_should_return_cached_instance()
+ {
+ // Arrange
+ const string authToken = "same-jwt-token";
+
+ // Act
+ var client1 = _factory.CreateClientWithJwtAuth(authToken);
+ var client2 = _factory.CreateClientWithJwtAuth(authToken);
+
+ // Assert
+ client1.Should().BeSameAs(client2, "same token should return cached instance");
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_different_tokens_should_return_different_instances()
+ {
+ // Arrange
+ const string token1 = "jwt-token-1";
+ const string token2 = "jwt-token-2";
+
+ // Act
+ var client1 = _factory.CreateClientWithJwtAuth(token1);
+ var client2 = _factory.CreateClientWithJwtAuth(token2);
+
+ // Assert
+ client1.Should().NotBeSameAs(client2, "different tokens should create different clients");
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_with_different_client_types_should_return_different_instances()
+ {
+ // Arrange
+ const string authToken = "jwt-token";
+
+ // Act
+ var validatorClient = _factory.CreateClientWithJwtAuth(authToken);
+ var walletClient = _factory.CreateClientWithJwtAuth(authToken);
+
+ // Assert
+ validatorClient.Should().NotBeSameAs(walletClient, "different client types should create different instances");
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_concurrent_calls_with_same_token_should_return_same_cached_instance()
+ {
+ // Arrange
+ const string authToken = "concurrent-jwt-token";
+ var clients = new System.Collections.Concurrent.ConcurrentBag();
+
+ // Act - Create 10 concurrent requests
+ Parallel.For(0, 10, _ =>
+ {
+ var client = _factory.CreateClientWithJwtAuth(authToken);
+ clients.Add(client);
+ });
+
+ // Assert - All should be the same cached instance
+ clients.Should().HaveCount(10);
+ clients.Distinct().Should().ContainSingle("all concurrent calls with same token should return the same cached instance");
+ }
+
+ [Fact]
+ public void CreateClientWithJwtAuth_after_cache_removal_should_create_new_instance()
+ {
+ // Arrange
+ const string authToken = "expiration-jwt-token";
+ var cacheKey = CantonApiClientFactory.GetJwtAuthCacheKey(authToken);
+
+ // Act
+ var client1 = _factory.CreateClientWithJwtAuth(authToken);
+ _memoryCache.Remove(cacheKey); // Simulate cache expiration
+ var client2 = _factory.CreateClientWithJwtAuth(authToken);
+
+ // Assert
+ client1.Should().NotBeSameAs(client2, "after cache removal, new instance should be created");
}
public void Dispose()