Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/Trakx.Canton.ApiClient/Auth/StaticJwtCredentialsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Net.Http.Headers;
using Trakx.Authentication;
using Trakx.Authentication.Client;

namespace Trakx.Canton.ApiClient;

/// <summary>
/// Provides static JWT credentials.
/// </summary>
public class StaticJwtCredentialsProvider<TAuthConfiguration> : IAuthCredentialsProvider<TAuthConfiguration>
{
private readonly string _authToken;

/// <summary>Constructor.</summary>
public StaticJwtCredentialsProvider(string authToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(authToken);
_authToken = authToken;
}

/// <inheritdoc />
public void AddCredentials(HttpRequestMessage msg)
{
var credentials = GetCredentials();
msg.Headers.Authorization = new AuthenticationHeaderValue(credentials.Scheme, credentials.Token);
}

/// <inheritdoc />
public Task AddCredentialsAsync(HttpRequestMessage msg, CancellationToken cancellationToken = default)
{
AddCredentials(msg);
return Task.CompletedTask;
}

/// <inheritdoc />
public AuthCredentials GetCredentials()
{
return new AuthCredentials
{
Scheme = AuthenticationConstants.CredentialsScheme.Bearer,
Token = _authToken
};
}

/// <inheritdoc />
public Task<AuthCredentials> GetCredentialsAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(GetCredentials());
}
}
51 changes: 39 additions & 12 deletions src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,9 +60,9 @@ public TClient CreateClient<TClient>(CantonApiClientCredentials credentials) whe
}

/// <inheritdoc />
public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClientBase
public TClient CreateClientWithJwtAuth<TClient>(string authToken) where TClient : ICantonApiClientBase
{
var cacheKey = GetJwtAuthCacheKey<TClient>();
var cacheKey = GetJwtAuthCacheKey<TClient>(authToken);

if (_cache.TryGetValue<TClient>(cacheKey, out var cachedClient))
return cachedClient!;
Expand All @@ -72,7 +73,8 @@ public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClie
if (_cache.TryGetValue(cacheKey, out cachedClient))
return cachedClient!;

var client = CreateClientWithJwtAuthInternal<TClient>();
var credentialsProvider = new StaticJwtCredentialsProvider<CantonApiClientConfiguration>(authToken);
var client = CreateClientInternal<TClient>(credentialsProvider);
_cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
return client;
}
Expand All @@ -82,6 +84,31 @@ public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClie
}
}

/// <inheritdoc />
public TClient CreateClientWithJwtAuthFromCurrentRequest<TClient>() where TClient : ICantonApiClientBase
{
var cacheKey = GetJwtAuthFromCurrentCacheKey<TClient>();

if (_cache.TryGetValue<TClient>(cacheKey, out var cachedClient))
return cachedClient!;

_jwtFromRequestAuthGate.Wait();
try
{
if (_cache.TryGetValue(cacheKey, out cachedClient))
return cachedClient!;

var credentialsProvider = new JwtCredentialsProvider<CantonApiClientConfiguration>(_httpContextAccessor);
var client = CreateClientInternal<TClient>(credentialsProvider);
_cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
return client;
}
finally
{
_jwtFromRequestAuthGate.Release();
}
}

private TClient CreateClientWithApiKeyAuthInternal<TClient>(CantonApiClientCredentials credentials)
where TClient : ICantonApiClientBase
{
Expand All @@ -95,13 +122,6 @@ private TClient CreateClientWithApiKeyAuthInternal<TClient>(CantonApiClientCrede
return CreateClientInternal<TClient>(credentialsProvider);
}

private TClient CreateClientWithJwtAuthInternal<TClient>()
where TClient : ICantonApiClientBase
{
var credentialsProvider = new JwtCredentialsProvider<CantonApiClientConfiguration>(_httpContextAccessor);
return CreateClientInternal<TClient>(credentialsProvider);
}

private TClient CreateClientInternal<TClient>(IAuthCredentialsProvider<CantonApiClientConfiguration> credentialsProvider) where TClient : ICantonApiClientBase
{
var clientType = typeof(TClient);
Expand All @@ -120,6 +140,13 @@ private TClient CreateClientInternal<TClient>(IAuthCredentialsProvider<CantonApi
internal static string GetApiKeyAuthCacheKey<TClient>(CantonApiClientCredentials credentials)
=> $"Canton_ApiClient_ApiKeyAuth_{typeof(TClient).FullName}_{credentials.ClientId}";

internal static string GetJwtAuthCacheKey<TClient>()
=> $"Canton_ApiClient_JwtAuth_{typeof(TClient).FullName}";
internal static string GetJwtAuthFromCurrentCacheKey<TClient>()
=> $"Canton_ApiClient_JwtAuthFromRequest_{typeof(TClient).FullName}";

internal static string GetJwtAuthCacheKey<TClient>(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}";
}
}
13 changes: 11 additions & 2 deletions src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ public interface ICantonApiClientFactory
TClient CreateClient<TClient>(CantonApiClientCredentials credentials) where TClient : ICantonApiClientBase;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TClient">The API client interface type.</typeparam>
/// <param name="authToken">The JWT token to use for authentication.</param>
/// <returns>A new instance of the API client configured with JWT authentication handler.</returns>
TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClientBase;
TClient CreateClientWithJwtAuth<TClient>(string authToken) where TClient : ICantonApiClientBase;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TClient">The API client interface type.</typeparam>
/// <returns>A new instance of the API client configured with JWT authentication handler.</returns>
TClient CreateClientWithJwtAuthFromCurrentRequest<TClient>() where TClient : ICantonApiClientBase;
}
Original file line number Diff line number Diff line change
@@ -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<CantonApiClientConfiguration>(token);

// Assert
provider.Should().NotBeNull();
}

[Fact]
public void Constructor_with_null_token_should_throw_ArgumentException()
{
// Act
var act = () => new StaticJwtCredentialsProvider<CantonApiClientConfiguration>(null!);

// Assert
act.Should().Throw<ArgumentException>();
}

[Fact]
public void Constructor_with_empty_token_should_throw_ArgumentException()
{
// Act
var act = () => new StaticJwtCredentialsProvider<CantonApiClientConfiguration>("");

// Assert
act.Should().Throw<ArgumentException>();
}

[Fact]
public void Constructor_with_whitespace_token_should_throw_ArgumentException()
{
// Act
var act = () => new StaticJwtCredentialsProvider<CantonApiClientConfiguration>(" ");

// Assert
act.Should().Throw<ArgumentException>();
}

[Fact]
public void GetCredentials_should_return_credentials_with_bearer_scheme()
{
// Arrange
const string token = "test-jwt-token";
var provider = new StaticJwtCredentialsProvider<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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<CantonApiClientConfiguration>(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);
}
}
Loading
Loading