From 033c843d4ae15e43a0ba0cd5b75e7d93cc21df1d Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:11:28 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=94=92Enable=20Authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TransformerBeeClient/RestClient.cs | 164 +++++++++++++++++- .../TransformerBeeClient.csproj | 2 + 2 files changed, 159 insertions(+), 7 deletions(-) diff --git a/TransformerBeeClient/TransformerBeeClient/RestClient.cs b/TransformerBeeClient/TransformerBeeClient/RestClient.cs index 024558d..f05547d 100644 --- a/TransformerBeeClient/TransformerBeeClient/RestClient.cs +++ b/TransformerBeeClient/TransformerBeeClient/RestClient.cs @@ -1,8 +1,13 @@ +using System.Net.Http.Headers; +using System.Security.Authentication; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using EDILibrary; using TransformerBeeClient.Model; +using IdentityModel.Client; +using System.IdentityModel.Tokens.Jwt; +using System.Net; namespace TransformerBeeClient; @@ -11,7 +16,32 @@ namespace TransformerBeeClient; /// public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact { + protected const string Auth0Domain = "https://hochfrequenz.eu.auth0.com"; + protected const string TransformerBeeScope = "client:default"; + protected const string TransformerAudience = "https://transformer.bee"; + + /// + /// JWT token + /// + private string? _token; + + + /// + /// true iff the client should use authentication + /// + private bool UseAuthentication { get; } + + private readonly string? _clientSecret; + private readonly string? _clientId; + + /// + /// prevents us from requesting multiple tokens at the same time + /// + private readonly SemaphoreSlim _tokenRequestSemaphore = new(1, 1); + + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true, @@ -20,17 +50,123 @@ public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact /// /// Provide the constructor with a http client factory. - /// It will create a client from said factory and use the for that. + /// It will create a client from said factory and use the for that. /// /// factory to create the http client from - /// name used to create the client + /// OAuth2 client ID + /// name used to create the client + /// OAuth2 client secret /// Find the OpenAPI Spec here: https://transformerstage.utilibee.io/swagger/index.html - public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, string clientName = "TransformerBee") + public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, string? clientId = null, string? clientSecret = null, string httpClientName = "TransformerBee") { - _httpClient = httpClientFactory.CreateClient(clientName); + _httpClient = httpClientFactory.CreateClient(httpClientName); if (_httpClient.BaseAddress == null) { - throw new ArgumentNullException(nameof(httpClientFactory), $"The http client factory must provide a base address for the client with name '{clientName}'"); + throw new ArgumentNullException(nameof(httpClientFactory), $"The http client factory must provide a base address for the client with name '{httpClientName}'"); + } + + if (!string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(clientSecret)) + { + throw new ArgumentNullException(nameof(clientSecret), $"If you provide a {nameof(clientId)} you must also provide a {nameof(clientSecret)}"); + } + + if (!string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(clientId)) + { + throw new ArgumentNullException(nameof(clientId), $"If you provide a {nameof(clientSecret)} you must also provide a {nameof(clientId)}"); + } + + UseAuthentication = !string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret); + if (UseAuthentication) + { + _clientId = clientId; + _clientSecret = clientSecret; + } + } + + /// + /// returns true if the will expire in the next 5 minutes + /// + /// jwt + /// true if the token should be refreshed + private static bool TokenNeedsToBeRefreshed(string token) + { + ArgumentNullException.ThrowIfNull(token); + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadToken(token) as JwtSecurityToken; + if (jwtToken == null) return true; // Assume renewal if token cannot be read + var expiry = jwtToken.ValidTo; + // Consider a buffer to renew the token a bit before it actually expires + var buffer = TimeSpan.FromMinutes(5); + return DateTime.UtcNow.Add(buffer) >= expiry; + } + + /// + /// gets the oauth2 discovery document from the respective auth0 domain + /// + /// + /// + private async Task GetDiscoveryDocument() + { + var discoveryDocument = await _httpClient.GetDiscoveryDocumentAsync(Auth0Domain); + if (discoveryDocument.IsError) + { + throw new AuthenticationException($"Could not get discovery document from auth0: {discoveryDocument.Error}"); + } + + return discoveryDocument; + } + + /// + /// fetch a token to talk to the transformer bee + /// + /// the token + /// if something goes wrong + private async Task GetTokenAsync() + { + using (_tokenRequestSemaphore) + { + if (!UseAuthentication) + { + throw new AuthenticationException("You must provide a client id and a client secret to the constructor to authenticate with the transformer bee"); + } + + if (!string.IsNullOrWhiteSpace(_token) && !TokenNeedsToBeRefreshed(_token)) + { + return _token; + } + + var discoveryDocument = await GetDiscoveryDocument(); + var tokenRequest = new ClientCredentialsTokenRequest + { + Address = discoveryDocument.TokenEndpoint, + ClientId = _clientId!, + ClientSecret = _clientSecret!, + GrantType = "client_credentials", + Scope = TransformerBeeScope, + }; + tokenRequest.Parameters.AddOptional("audience", TransformerAudience); + + var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(tokenRequest); + if (tokenResponse.IsError) + { + throw new AuthenticationException($"Could not get token from auth0: {tokenResponse.Error}"); + } + + _token = tokenResponse.AccessToken!; + } + + return _token; + } + + /// + /// Make sure that the client is authenticated, if necessary + /// + private async Task EnsureAuthentication() + { + if (UseAuthentication) + { + var token = await GetTokenAsync(); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } } @@ -69,6 +205,7 @@ public async Task> ConvertToBo4e(string edifact, EdifactFor { throw new ArgumentNullException(nameof(edifact)); } + var uriBuilder = new UriBuilder(_httpClient!.BaseAddress!) { Path = "/v1/transformer/EdiToBo4E" @@ -81,12 +218,18 @@ public async Task> ConvertToBo4e(string edifact, EdifactFor FormatVersion = formatVersion, }; var requestJson = JsonSerializer.Serialize(request, _jsonSerializerOptions); + await EnsureAuthentication(); var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - // e.g. 401 + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !UseAuthentication) + { + throw new AuthenticationException("You have to provide client id and client secret to the constructor to authenticate yourself against transformer bee."); + } + throw new HttpRequestException($"Could not convert {edifact} to BO4E. Status code: {httpResponse.StatusCode}"); } + var responseContent = await httpResponse.Content.ReadAsStringAsync(); var bo4eResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); // todo: handle the case that the deserialization fails and bo4eResponse is null @@ -102,6 +245,7 @@ public async Task ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVer { throw new ArgumentNullException(nameof(boneyComb)); } + var uriBuilder = new UriBuilder(_httpClient!.BaseAddress!) { Path = "/v1/transformer/Bo4ETransactionToEdi" @@ -115,12 +259,18 @@ public async Task ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVer FormatVersion = formatVersion, }; var requestJson = JsonSerializer.Serialize(request, _jsonSerializerOptions); + await EnsureAuthentication(); var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - // e.g. 401 + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !UseAuthentication) + { + throw new AuthenticationException("You have to provide client id and client secret to the constructor to authenticate yourself against transformer bee."); + } + throw new HttpRequestException($"Could not convert to EDIFACT; Status code: {httpResponse.StatusCode}"); } + var responseContent = await httpResponse.Content.ReadAsStringAsync(); // todo: ensure that the deserialization does not fail and the response is not empty var responseBody = JsonSerializer.Deserialize(responseContent!, _jsonSerializerOptions); diff --git a/TransformerBeeClient/TransformerBeeClient/TransformerBeeClient.csproj b/TransformerBeeClient/TransformerBeeClient/TransformerBeeClient.csproj index 42d5454..5489d3b 100644 --- a/TransformerBeeClient/TransformerBeeClient/TransformerBeeClient.csproj +++ b/TransformerBeeClient/TransformerBeeClient/TransformerBeeClient.csproj @@ -13,7 +13,9 @@ + + From 7c20429f545f46a269231895d004530ade977cf6 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:40:31 +0000 Subject: [PATCH 02/13] restructure --- .../Bo4eToEdifactTests.cs | 4 +- .../ClientFixture.cs | 3 + .../ConnectionTests.cs | 6 +- .../EdifactToBo4eTests.cs | 5 +- .../ConnectionTests.cs | 2 +- .../TransformerBeeClient/Class1.cs | 5 - .../ClientIdClientSecretTokenProvider.cs | 136 +++++++++++++++++ .../TransformerBeeClient/Interfaces.cs | 18 +++ .../NoAuthenticationProvider.cs | 14 ++ .../TransformerBeeClient/RestClient.cs | 140 ++---------------- 10 files changed, 196 insertions(+), 137 deletions(-) delete mode 100644 TransformerBeeClient/TransformerBeeClient/Class1.cs create mode 100644 TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs create mode 100644 TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs index 63079dd..a16767a 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs @@ -13,17 +13,19 @@ namespace TransformerBeeClient.IntegrationTest; public class Bo4eToEdifactTests : IClassFixture { private readonly ClientFixture _client; + private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; public Bo4eToEdifactTests(ClientFixture clientFixture) { _client = clientFixture; + _authenticationProvider = clientFixture.AuthenticationProvider; } [Fact] public async Task BOneyComb_Can_Be_Converted_To_Edifact() { var httpClientFactory = _client.HttpClientFactory; - ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory); + ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); var boneyCombString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.json"); var deserializerOptions = new JsonSerializerOptions { diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs index 9556601..cbc8b99 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs @@ -12,6 +12,8 @@ public class ClientFixture : IClassFixture public readonly ServiceCollection ServiceCollection; + public readonly ITransformerBeeAuthenticationProvider AuthenticationProvider; + public ClientFixture() { var services = new ServiceCollection(); @@ -22,5 +24,6 @@ public ClientFixture() var serviceProvider = services.BuildServiceProvider(); ServiceCollection = services; HttpClientFactory = serviceProvider.GetService(); + AuthenticationProvider = new NoAuthenticationprovider(); // easy for integration tests with transformer.bee running in docker } } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs index 50e81a2..f2d2030 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs @@ -11,17 +11,19 @@ public class ConnectionTests : IClassFixture { private readonly ClientFixture _client; + private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; public ConnectionTests(ClientFixture clientFixture) { _client = clientFixture; + _authenticationProvider = clientFixture.AuthenticationProvider; } [Fact] public async Task IsAvailable_Returns_True_If_Service_Is_Available() { var httpClientFactory = _client.HttpClientFactory; - var client = new TransformerBeeRestClient(httpClientFactory); + var client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); var result = await client.IsAvailable(); result.Should().BeTrue(); } @@ -35,7 +37,7 @@ public async Task IsAvailable_Throws_Exception_If_Host_Is_Unavailable() client.BaseAddress = new Uri("http://localhost:1234"); // <-- no service running under this address }); var serviceProvider = services.BuildServiceProvider(); - var client = new TransformerBeeRestClient(serviceProvider.GetService()); + var client = new TransformerBeeRestClient(serviceProvider.GetService(), _authenticationProvider); var checkIfIsAvailable = async () => await client.IsAvailable(); await checkIfIsAvailable.Should().ThrowAsync(); } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index e18f0ed..e7b68b0 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -11,17 +11,20 @@ namespace TransformerBeeClient.IntegrationTest; public class EdifactToBo4eTests : IClassFixture { private readonly ClientFixture _client; + + private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; public EdifactToBo4eTests(ClientFixture clientFixture) { _client = clientFixture; + _authenticationProvider = clientFixture.AuthenticationProvider; } [Fact] public async Task Edifact_Can_Be_Converted_To_Bo4e() { var httpClientFactory = _client.HttpClientFactory; - ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory); + ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); var edifactString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.edi"); var result = await client.ConvertToBo4e(edifactString, EdifactFormatVersion.FV2310); result.Should().BeOfType>(); diff --git a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs index e770442..6f50073 100644 --- a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs @@ -15,7 +15,7 @@ public void IsAvailable_Throws_ArgumentNullException_If_BaseAddress_Is_Not_Confi client.BaseAddress = null; }); var serviceProvider = services.BuildServiceProvider(); - var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService()); + var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService(), new NoAuthenticationprovider()); instantiateClient.Should().Throw(); } } diff --git a/TransformerBeeClient/TransformerBeeClient/Class1.cs b/TransformerBeeClient/TransformerBeeClient/Class1.cs deleted file mode 100644 index 768b84c..0000000 --- a/TransformerBeeClient/TransformerBeeClient/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace TransformerBeeClient; - -public class Class1 -{ -} \ No newline at end of file diff --git a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs new file mode 100644 index 0000000..2616d2f --- /dev/null +++ b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs @@ -0,0 +1,136 @@ +namespace TransformerBeeClient; + +using System.Security.Authentication; +using IdentityModel.Client; +using System.IdentityModel.Tokens.Jwt; + +/// +/// a that is based on a client secret and a client id +/// +public class ClientIdClientSecretAuthenticationProvider : ITransformerBeeAuthenticationProvider +{ + protected const string Auth0Domain = "https://hochfrequenz.eu.auth0.com"; + protected const string TransformerBeeScope = "client:default"; + protected const string TransformerAudience = "https://transformer.bee"; + + /// + /// JWT token + /// + private string? _token; + + private readonly string? _clientSecret; + private readonly string? _clientId; + + /// + /// true iff the client should use authentication + /// + private readonly bool _useAuthentication; + + /// + /// + /// + public bool UseAuthentication() => _useAuthentication; + + /// + /// prevents us from requesting multiple tokens at the same time + /// + private readonly SemaphoreSlim _tokenRequestSemaphore = new(1, 1); + + /// A that is based on a client secret and a client id + /// OAuth2 client secret (ask Hochfrequenz) + /// OAuth2 client ID (ask Hochfrequenz) + public ClientIdClientSecretAuthenticationProvider(string? clientId, string? clientSecret) + { + if (!string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(clientSecret)) + { + throw new ArgumentNullException(nameof(clientSecret), $"If you provide a {nameof(clientId)} you must also provide a {nameof(clientSecret)}"); + } + + if (!string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(clientId)) + { + throw new ArgumentNullException(nameof(clientId), $"If you provide a {nameof(clientSecret)} you must also provide a {nameof(clientId)}"); + } + + _useAuthentication = !string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret); + if (_useAuthentication) + { + _clientId = clientId; + _clientSecret = clientSecret; + } + } + + /// + /// returns true if the will expire in the next 5 minutes + /// + /// jwt + /// true if the token should be refreshed + private static bool TokenNeedsToBeRefreshed(string token) + { + ArgumentNullException.ThrowIfNull(token); + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadToken(token) as JwtSecurityToken; + if (jwtToken == null) return true; // Assume renewal if token cannot be read + var expiry = jwtToken.ValidTo; + // Consider a buffer to renew the token a bit before it actually expires + var buffer = TimeSpan.FromMinutes(5); + return DateTime.UtcNow.Add(buffer) >= expiry; + } + + /// + /// gets the oauth2 discovery document from the respective auth0 domain + /// + /// + /// + private static async Task GetDiscoveryDocument(HttpClient client) + { + var discoveryDocument = await client.GetDiscoveryDocumentAsync(Auth0Domain); + if (discoveryDocument.IsError) + { + throw new AuthenticationException($"Could not get discovery document from auth0: {discoveryDocument.Error}"); + } + + return discoveryDocument; + } + + /// + /// fetch a token to talk to the transformer bee + /// + /// the token + /// if something goes wrong + public async Task GetTokenAsync(HttpClient client) + { + using (_tokenRequestSemaphore) + { + if (!UseAuthentication()) + { + throw new AuthenticationException("You must provide a client id and a client secret to the constructor to authenticate with the transformer bee"); + } + + if (!string.IsNullOrWhiteSpace(_token) && !TokenNeedsToBeRefreshed(_token)) + { + return _token; + } + + var discoveryDocument = await GetDiscoveryDocument(client); + var tokenRequest = new ClientCredentialsTokenRequest + { + Address = discoveryDocument.TokenEndpoint, + ClientId = _clientId!, + ClientSecret = _clientSecret!, + GrantType = "client_credentials", + Scope = TransformerBeeScope, + }; + tokenRequest.Parameters.AddOptional("audience", TransformerAudience); + + var tokenResponse = await client.RequestClientCredentialsTokenAsync(tokenRequest); + if (tokenResponse.IsError) + { + throw new AuthenticationException($"Could not get token from auth0: {tokenResponse.Error}"); + } + + _token = tokenResponse.AccessToken!; + } + + return _token; + } +} diff --git a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs index 0398e0e..b28f8dc 100644 --- a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs +++ b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs @@ -32,3 +32,21 @@ public interface ICanConvertToEdifact /// the edifact as plain string public Task ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVersion formatVersion); } + +/// +/// Can provide information on whether you need to authenticate against transformer.bee and how +/// +public interface ITransformerBeeAuthenticationProvider +{ + /// + /// returns true iff the client should use authentication + /// + /// + public bool UseAuthentication(); + + /// + /// provides the token to authenticate against transformer.bee (if and only if is true) + /// + /// + public Task GetTokenAsync(HttpClient client); +} diff --git a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs new file mode 100644 index 0000000..769c847 --- /dev/null +++ b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs @@ -0,0 +1,14 @@ +namespace TransformerBeeClient; + +/// +/// a stub that just indicates, we don't need to authenticate against transformer.bee, because we're e.g. in the same network +/// +public class NoAuthenticationprovider : ITransformerBeeAuthenticationProvider +{ + public bool UseAuthentication() => false; + + public Task GetTokenAsync(HttpClient client) + { + throw new NotImplementedException("This must never be called, because we don't use authentication."); + } +} diff --git a/TransformerBeeClient/TransformerBeeClient/RestClient.cs b/TransformerBeeClient/TransformerBeeClient/RestClient.cs index f05547d..d9a60df 100644 --- a/TransformerBeeClient/TransformerBeeClient/RestClient.cs +++ b/TransformerBeeClient/TransformerBeeClient/RestClient.cs @@ -1,13 +1,11 @@ -using System.Net.Http.Headers; using System.Security.Authentication; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using EDILibrary; using TransformerBeeClient.Model; -using IdentityModel.Client; -using System.IdentityModel.Tokens.Jwt; using System.Net; +using System.Net.Http.Headers; namespace TransformerBeeClient; @@ -16,30 +14,7 @@ namespace TransformerBeeClient; /// public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact { - protected const string Auth0Domain = "https://hochfrequenz.eu.auth0.com"; - protected const string TransformerBeeScope = "client:default"; - protected const string TransformerAudience = "https://transformer.bee"; - - /// - /// JWT token - /// - private string? _token; - - - /// - /// true iff the client should use authentication - /// - private bool UseAuthentication { get; } - - private readonly string? _clientSecret; - private readonly string? _clientId; - - /// - /// prevents us from requesting multiple tokens at the same time - /// - private readonly SemaphoreSlim _tokenRequestSemaphore = new(1, 1); - - + private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions = new() @@ -53,11 +28,10 @@ public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact /// It will create a client from said factory and use the for that. /// /// factory to create the http client from - /// OAuth2 client ID + /// something that tells you whether and how you need to authenticate yourself at transformer.bee /// name used to create the client - /// OAuth2 client secret /// Find the OpenAPI Spec here: https://transformerstage.utilibee.io/swagger/index.html - public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, string? clientId = null, string? clientSecret = null, string httpClientName = "TransformerBee") + public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, ITransformerBeeAuthenticationProvider authenticationProvider, string httpClientName = "TransformerBee") { _httpClient = httpClientFactory.CreateClient(httpClientName); if (_httpClient.BaseAddress == null) @@ -65,97 +39,7 @@ public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, string? cl throw new ArgumentNullException(nameof(httpClientFactory), $"The http client factory must provide a base address for the client with name '{httpClientName}'"); } - if (!string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(clientSecret)) - { - throw new ArgumentNullException(nameof(clientSecret), $"If you provide a {nameof(clientId)} you must also provide a {nameof(clientSecret)}"); - } - - if (!string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(clientId)) - { - throw new ArgumentNullException(nameof(clientId), $"If you provide a {nameof(clientSecret)} you must also provide a {nameof(clientId)}"); - } - - UseAuthentication = !string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret); - if (UseAuthentication) - { - _clientId = clientId; - _clientSecret = clientSecret; - } - } - - /// - /// returns true if the will expire in the next 5 minutes - /// - /// jwt - /// true if the token should be refreshed - private static bool TokenNeedsToBeRefreshed(string token) - { - ArgumentNullException.ThrowIfNull(token); - var handler = new JwtSecurityTokenHandler(); - var jwtToken = handler.ReadToken(token) as JwtSecurityToken; - if (jwtToken == null) return true; // Assume renewal if token cannot be read - var expiry = jwtToken.ValidTo; - // Consider a buffer to renew the token a bit before it actually expires - var buffer = TimeSpan.FromMinutes(5); - return DateTime.UtcNow.Add(buffer) >= expiry; - } - - /// - /// gets the oauth2 discovery document from the respective auth0 domain - /// - /// - /// - private async Task GetDiscoveryDocument() - { - var discoveryDocument = await _httpClient.GetDiscoveryDocumentAsync(Auth0Domain); - if (discoveryDocument.IsError) - { - throw new AuthenticationException($"Could not get discovery document from auth0: {discoveryDocument.Error}"); - } - - return discoveryDocument; - } - - /// - /// fetch a token to talk to the transformer bee - /// - /// the token - /// if something goes wrong - private async Task GetTokenAsync() - { - using (_tokenRequestSemaphore) - { - if (!UseAuthentication) - { - throw new AuthenticationException("You must provide a client id and a client secret to the constructor to authenticate with the transformer bee"); - } - - if (!string.IsNullOrWhiteSpace(_token) && !TokenNeedsToBeRefreshed(_token)) - { - return _token; - } - - var discoveryDocument = await GetDiscoveryDocument(); - var tokenRequest = new ClientCredentialsTokenRequest - { - Address = discoveryDocument.TokenEndpoint, - ClientId = _clientId!, - ClientSecret = _clientSecret!, - GrantType = "client_credentials", - Scope = TransformerBeeScope, - }; - tokenRequest.Parameters.AddOptional("audience", TransformerAudience); - - var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(tokenRequest); - if (tokenResponse.IsError) - { - throw new AuthenticationException($"Could not get token from auth0: {tokenResponse.Error}"); - } - - _token = tokenResponse.AccessToken!; - } - - return _token; + _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); } /// @@ -163,9 +47,9 @@ private async Task GetTokenAsync() /// private async Task EnsureAuthentication() { - if (UseAuthentication) + if (_authenticationProvider.UseAuthentication()) { - var token = await GetTokenAsync(); + var token = await _authenticationProvider.GetTokenAsync(_httpClient); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } } @@ -189,6 +73,8 @@ public async Task IsAvailable() var versionUrl = uriBuilder.Uri.AbsoluteUri; var response = await _httpClient.GetAsync(versionUrl); + // note that this is available without any authentication + // see e.g. http://transformerstage.utilibee.io/version return response.IsSuccessStatusCode; } @@ -222,9 +108,9 @@ public async Task> ConvertToBo4e(string edifact, EdifactFor var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !UseAuthentication) + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticationProvider.UseAuthentication()) { - throw new AuthenticationException("You have to provide client id and client secret to the constructor to authenticate yourself against transformer bee."); + throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticationProvider)}?"); } throw new HttpRequestException($"Could not convert {edifact} to BO4E. Status code: {httpResponse.StatusCode}"); @@ -263,9 +149,9 @@ public async Task ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVer var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !UseAuthentication) + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticationProvider.UseAuthentication()) { - throw new AuthenticationException("You have to provide client id and client secret to the constructor to authenticate yourself against transformer bee."); + throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticationProvider)}?"); } throw new HttpRequestException($"Could not convert to EDIFACT; Status code: {httpResponse.StatusCode}"); From a9aec3612464c02bca6d7d78d498a2b5373b4e9d Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:42:21 +0000 Subject: [PATCH 03/13] format --- .../TransformerBeeClient.IntegrationTest/ClientFixture.cs | 2 +- .../TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs index cbc8b99..dcbecfb 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs @@ -13,7 +13,7 @@ public class ClientFixture : IClassFixture public readonly ServiceCollection ServiceCollection; public readonly ITransformerBeeAuthenticationProvider AuthenticationProvider; - + public ClientFixture() { var services = new ServiceCollection(); diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index e7b68b0..741041f 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -11,7 +11,7 @@ namespace TransformerBeeClient.IntegrationTest; public class EdifactToBo4eTests : IClassFixture { private readonly ClientFixture _client; - + private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; public EdifactToBo4eTests(ClientFixture clientFixture) From d65118ad6283a61cade13a07c553a49e00ef41c6 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:42:57 +0000 Subject: [PATCH 04/13] boms --- .../TransformerBeeClient/ClientIdClientSecretTokenProvider.cs | 2 +- TransformerBeeClient/TransformerBeeClient/Interfaces.cs | 2 +- .../TransformerBeeClient/NoAuthenticationProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs index 2616d2f..a8aa9f4 100644 --- a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs +++ b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs @@ -1,4 +1,4 @@ -namespace TransformerBeeClient; +namespace TransformerBeeClient; using System.Security.Authentication; using IdentityModel.Client; diff --git a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs index b28f8dc..d74cae7 100644 --- a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs +++ b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs @@ -43,7 +43,7 @@ public interface ITransformerBeeAuthenticationProvider /// /// public bool UseAuthentication(); - + /// /// provides the token to authenticate against transformer.bee (if and only if is true) /// diff --git a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs index 769c847..aed1795 100644 --- a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs +++ b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs @@ -1,4 +1,4 @@ -namespace TransformerBeeClient; +namespace TransformerBeeClient; /// /// a stub that just indicates, we don't need to authenticate against transformer.bee, because we're e.g. in the same network From f9c19ab4bae83a6b3d67d87b333d5e2e9c244a4b Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:47:06 +0000 Subject: [PATCH 05/13] typo and docs --- README.md | 4 ++++ .../TransformerBeeClient.IntegrationTest/ClientFixture.cs | 2 +- .../TransformerBeeClient.UnitTest/ConnectionTests.cs | 2 +- .../TransformerBeeClient/NoAuthenticationProvider.cs | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebe5d64..cbad941 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ dotnet add package TransformerBeeClient ### Use it in your code +#### Authentication +You need to provide something that implements `ITransformerBeeAuthenticator` to the `TransformerBeeClient`. +If you're hosting transformer.bee in the same network and there is no authentication, you can use the `NoAuthenticationProvider` class. + Then, you can use the client like this: ```csharp diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs index dcbecfb..9223881 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs @@ -24,6 +24,6 @@ public ClientFixture() var serviceProvider = services.BuildServiceProvider(); ServiceCollection = services; HttpClientFactory = serviceProvider.GetService(); - AuthenticationProvider = new NoAuthenticationprovider(); // easy for integration tests with transformer.bee running in docker + AuthenticationProvider = new NoAuthenticationProvider(); // easy for integration tests with transformer.bee running in docker } } diff --git a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs index 6f50073..8614bf9 100644 --- a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs @@ -15,7 +15,7 @@ public void IsAvailable_Throws_ArgumentNullException_If_BaseAddress_Is_Not_Confi client.BaseAddress = null; }); var serviceProvider = services.BuildServiceProvider(); - var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService(), new NoAuthenticationprovider()); + var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService(), new NoAuthenticationProvider()); instantiateClient.Should().Throw(); } } diff --git a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs index aed1795..3c97cde 100644 --- a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs +++ b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs @@ -3,7 +3,7 @@ namespace TransformerBeeClient; /// /// a stub that just indicates, we don't need to authenticate against transformer.bee, because we're e.g. in the same network /// -public class NoAuthenticationprovider : ITransformerBeeAuthenticationProvider +public class NoAuthenticationProvider : ITransformerBeeAuthenticationProvider { public bool UseAuthentication() => false; From ec7deb8033b54fd86270a134aaf4040d6a6e784e Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:49:20 +0000 Subject: [PATCH 06/13] reanme --- ...nProvider.cs => ClientIdClientSecretAuthenticationProvider.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename TransformerBeeClient/TransformerBeeClient/{ClientIdClientSecretTokenProvider.cs => ClientIdClientSecretAuthenticationProvider.cs} (100%) diff --git a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs similarity index 100% rename from TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretTokenProvider.cs rename to TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs From 8704eea67fe68f358b8c6c0148ed4028144d70de Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 19:52:32 +0000 Subject: [PATCH 07/13] more docs --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cbad941..694c6ac 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,24 @@ Install it from nuget [TransformerBeeClient](https://www.nuget.org/packages/Tran dotnet add package TransformerBeeClient ``` -### Use it in your code +### Authentication +You need to provide something that implements `ITransformerBeeAuthenticationProvider` to the `TransformerBeeClient`. -#### Authentication -You need to provide something that implements `ITransformerBeeAuthenticator` to the `TransformerBeeClient`. -If you're hosting transformer.bee in the same network and there is no authentication, you can use the `NoAuthenticationProvider` class. +#### No Authentication +If you're hosting transformer.bee in the same network and there is no authentication, you can use the `NoAuthenticationProvider`. +```csharp +using TransformerBeeClient; +var myAuthenticator = new NoAuthenticationProvider(); +``` -Then, you can use the client like this: +#### OAuth2 Client and Secret +If, which is more likely, Hochfrequenz provided you with a client ID and secret, you can use the `ClientIdClientSecretAuthenticationProvider` class like this: +```csharp +using TransformerBeeClient; +var myAuthenticator = new ClientIdClientSecretAuthenticationProvider("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); +``` +...todo ```csharp ``` From 417c28e7e2f6e11e70a1a8e390a22f3c27507197 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 20:03:14 +0000 Subject: [PATCH 08/13] try? --- .github/workflows/integrationtests.yml | 7 ++++ .../EdifactToBo4eTests.cs | 31 +++++++++++++++++ .../GithubActionClientFixture.cs | 34 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs diff --git a/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml index 6c1d5ab..3be05e3 100644 --- a/.github/workflows/integrationtests.yml +++ b/.github/workflows/integrationtests.yml @@ -48,3 +48,10 @@ jobs: working-directory: TransformerBeeClient/TransformerBeeClient.IntegrationTest run: | dotnet test + - name: Run Integration Tests (against real service) + working-directory: TransformerBeeClient/TransformerBeeClient.IntegrationTest + env: + CLIENT_ID: ${{ secrets.AUTH0_TEST_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.AUTH0_TEST_CLIENT_SECRET }} + run: | + dotnet test diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index 741041f..9767bdd 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -31,3 +31,34 @@ public async Task Edifact_Can_Be_Converted_To_Bo4e() result.Single().Transaktionen.Should().NotBeNullOrEmpty(); } } + +/// +/// those tests only run if the environment variables are set up correctly +/// +public class EdifactToBo4eTestsWithAuthentication : IClassFixture +{ + private readonly GithubActionClientFixture _client; + + private readonly ITransformerBeeAuthenticationProvider? _authenticationProvider; + + public EdifactToBo4eTestsWithAuthentication(GithubActionClientFixture clientFixture) + { + _client = clientFixture; + _authenticationProvider = clientFixture.AuthenticationProvider; + } + + [Fact] + public async Task Edifact_Can_Be_Converted_To_Bo4e_With_Authentication() + { + if (_authenticationProvider is null) + { + return; // skip the test if no authentication provider is available + } + var httpClientFactory = _client.HttpClientFactory; + ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); + var edifactString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.edi"); + var result = await client.ConvertToBo4e(edifactString, EdifactFormatVersion.FV2310); + result.Should().BeOfType>(); + result.Single().Transaktionen.Should().NotBeNullOrEmpty(); + } +} diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs new file mode 100644 index 0000000..58260e9 --- /dev/null +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace TransformerBeeClient.IntegrationTest; + +/// +/// A fixture that sets up the http client factory and an injectable service collection. +/// It's thought to be used in the Github action. +/// +public class GithubActionClientFixture : IClassFixture +{ + public readonly IHttpClientFactory? HttpClientFactory; + + public readonly ServiceCollection? ServiceCollection; + + public readonly ITransformerBeeAuthenticationProvider? AuthenticationProvider; + + public GithubActionClientFixture() + { + var clientId = Environment.GetEnvironmentVariable("CLIENT_ID"); + var clientSecret = Environment.GetEnvironmentVariable("CLIENT_SECRET"); + if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret)) + { + return; + } + + var services = new ServiceCollection(); + services.AddHttpClient("TransformerBee", client => { client.BaseAddress = new Uri("http://transformerstage.utilibee.io"); }); + var serviceProvider = services.BuildServiceProvider(); + ServiceCollection = services; + HttpClientFactory = serviceProvider.GetService(); + AuthenticationProvider = new ClientIdClientSecretAuthenticationProvider(clientId: clientId, clientSecret: clientSecret); + } +} From ac42692fe617cb478f111abe45a96830298e2b19 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 20:06:26 +0000 Subject: [PATCH 09/13] so? --- .../TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs | 3 ++- .../GithubActionClientFixture.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index 9767bdd..4180ec8 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -52,7 +52,8 @@ public async Task Edifact_Can_Be_Converted_To_Bo4e_With_Authentication() { if (_authenticationProvider is null) { - return; // skip the test if no authentication provider is available + await Console.Out.WriteLineAsync("Skipping tests because no client id or client secret is available"); + return; } var httpClientFactory = _client.HttpClientFactory; ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs index 58260e9..3dedfa8 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs @@ -21,6 +21,7 @@ public GithubActionClientFixture() var clientSecret = Environment.GetEnvironmentVariable("CLIENT_SECRET"); if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret)) { + Console.Out.WriteLine("Skipping tests because no client id or client secret is available"); return; } From 39ae8c6fb837da9c9bc6b7faa36c0da962c530e9 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 1 Feb 2024 20:07:00 +0000 Subject: [PATCH 10/13] s --- .../TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index 4180ec8..2cdbce3 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -61,5 +61,6 @@ public async Task Edifact_Can_Be_Converted_To_Bo4e_With_Authentication() var result = await client.ConvertToBo4e(edifactString, EdifactFormatVersion.FV2310); result.Should().BeOfType>(); result.Single().Transaktionen.Should().NotBeNullOrEmpty(); + await Console.Out.WriteLineAsync("Successfully converted edifact to bo4e - with authentication!"); } } From a6f80a35e39a071cace4b16e6db4d4f0beb1ae55 Mon Sep 17 00:00:00 2001 From: konstantin Date: Fri, 2 Feb 2024 06:53:33 +0000 Subject: [PATCH 11/13] better --- .../Bo4eToEdifactTests.cs | 6 ++--- .../ClientFixture.cs | 4 ++-- .../ConnectionTests.cs | 8 +++---- .../EdifactToBo4eTests.cs | 22 +++++++++++-------- .../GithubActionClientFixture.cs | 8 +++---- ...ransformerBeeClient.IntegrationTest.csproj | 1 + .../ConnectionTests.cs | 2 +- ...entIdClientSecretAuthenticationProvider.cs | 12 +++++----- .../TransformerBeeClient/Interfaces.cs | 4 ++-- .../NoAuthenticationProvider.cs | 6 ++--- .../TransformerBeeClient/RestClient.cs | 20 ++++++++--------- 11 files changed, 48 insertions(+), 45 deletions(-) diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs index a16767a..deb5f23 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/Bo4eToEdifactTests.cs @@ -13,19 +13,19 @@ namespace TransformerBeeClient.IntegrationTest; public class Bo4eToEdifactTests : IClassFixture { private readonly ClientFixture _client; - private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; + private readonly ITransformerBeeAuthenticator _authenticator; public Bo4eToEdifactTests(ClientFixture clientFixture) { _client = clientFixture; - _authenticationProvider = clientFixture.AuthenticationProvider; + _authenticator = clientFixture.Authenticator; } [Fact] public async Task BOneyComb_Can_Be_Converted_To_Edifact() { var httpClientFactory = _client.HttpClientFactory; - ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); + ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory, _authenticator); var boneyCombString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.json"); var deserializerOptions = new JsonSerializerOptions { diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs index 9223881..1dec46b 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ClientFixture.cs @@ -12,7 +12,7 @@ public class ClientFixture : IClassFixture public readonly ServiceCollection ServiceCollection; - public readonly ITransformerBeeAuthenticationProvider AuthenticationProvider; + public readonly ITransformerBeeAuthenticator Authenticator; public ClientFixture() { @@ -24,6 +24,6 @@ public ClientFixture() var serviceProvider = services.BuildServiceProvider(); ServiceCollection = services; HttpClientFactory = serviceProvider.GetService(); - AuthenticationProvider = new NoAuthenticationProvider(); // easy for integration tests with transformer.bee running in docker + Authenticator = new NoAuthenticator(); // easy for integration tests with transformer.bee running in docker } } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs index f2d2030..bdc0f28 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/ConnectionTests.cs @@ -11,19 +11,19 @@ public class ConnectionTests : IClassFixture { private readonly ClientFixture _client; - private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; + private readonly ITransformerBeeAuthenticator _authenticator; public ConnectionTests(ClientFixture clientFixture) { _client = clientFixture; - _authenticationProvider = clientFixture.AuthenticationProvider; + _authenticator = clientFixture.Authenticator; } [Fact] public async Task IsAvailable_Returns_True_If_Service_Is_Available() { var httpClientFactory = _client.HttpClientFactory; - var client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); + var client = new TransformerBeeRestClient(httpClientFactory, _authenticator); var result = await client.IsAvailable(); result.Should().BeTrue(); } @@ -37,7 +37,7 @@ public async Task IsAvailable_Throws_Exception_If_Host_Is_Unavailable() client.BaseAddress = new Uri("http://localhost:1234"); // <-- no service running under this address }); var serviceProvider = services.BuildServiceProvider(); - var client = new TransformerBeeRestClient(serviceProvider.GetService(), _authenticationProvider); + var client = new TransformerBeeRestClient(serviceProvider.GetService(), _authenticator); var checkIfIsAvailable = async () => await client.IsAvailable(); await checkIfIsAvailable.Should().ThrowAsync(); } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index 2cdbce3..b1c599a 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using TransformerBeeClient.Model; using Xunit; +using Xunit.Abstractions; namespace TransformerBeeClient.IntegrationTest; @@ -12,19 +13,19 @@ public class EdifactToBo4eTests : IClassFixture { private readonly ClientFixture _client; - private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; + private readonly ITransformerBeeAuthenticator _authenticator; public EdifactToBo4eTests(ClientFixture clientFixture) { _client = clientFixture; - _authenticationProvider = clientFixture.AuthenticationProvider; + _authenticator = clientFixture.Authenticator; } [Fact] public async Task Edifact_Can_Be_Converted_To_Bo4e() { var httpClientFactory = _client.HttpClientFactory; - ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory, _authenticationProvider); + ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory, _authenticator); var edifactString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.edi"); var result = await client.ConvertToBo4e(edifactString, EdifactFormatVersion.FV2310); result.Should().BeOfType>(); @@ -39,28 +40,31 @@ public class EdifactToBo4eTestsWithAuthentication : IClassFixture>(); result.Single().Transaktionen.Should().NotBeNullOrEmpty(); - await Console.Out.WriteLineAsync("Successfully converted edifact to bo4e - with authentication!"); + _output.WriteLine("Successfully converted edifact to bo4e - with authentication!"); } } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs index 3dedfa8..0f8fb79 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/GithubActionClientFixture.cs @@ -13,23 +13,21 @@ public class GithubActionClientFixture : IClassFixture { client.BaseAddress = new Uri("http://transformerstage.utilibee.io"); }); var serviceProvider = services.BuildServiceProvider(); ServiceCollection = services; HttpClientFactory = serviceProvider.GetService(); - AuthenticationProvider = new ClientIdClientSecretAuthenticationProvider(clientId: clientId, clientSecret: clientSecret); + AuthenticationProvider = new ClientIdClientSecretAuthenticator(clientId: clientId, clientSecret: clientSecret); } } diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/TransformerBeeClient.IntegrationTest.csproj b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/TransformerBeeClient.IntegrationTest.csproj index 8b85110..a43ba73 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/TransformerBeeClient.IntegrationTest.csproj +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/TransformerBeeClient.IntegrationTest.csproj @@ -14,6 +14,7 @@ + diff --git a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs index 8614bf9..ae92893 100644 --- a/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.UnitTest/ConnectionTests.cs @@ -15,7 +15,7 @@ public void IsAvailable_Throws_ArgumentNullException_If_BaseAddress_Is_Not_Confi client.BaseAddress = null; }); var serviceProvider = services.BuildServiceProvider(); - var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService(), new NoAuthenticationProvider()); + var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService(), new NoAuthenticator()); instantiateClient.Should().Throw(); } } diff --git a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs index a8aa9f4..43845dd 100644 --- a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs +++ b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs @@ -5,9 +5,9 @@ namespace TransformerBeeClient; using System.IdentityModel.Tokens.Jwt; /// -/// a that is based on a client secret and a client id +/// a that is based on a client secret and a client id /// -public class ClientIdClientSecretAuthenticationProvider : ITransformerBeeAuthenticationProvider +public class ClientIdClientSecretAuthenticator : ITransformerBeeAuthenticator { protected const string Auth0Domain = "https://hochfrequenz.eu.auth0.com"; protected const string TransformerBeeScope = "client:default"; @@ -27,7 +27,7 @@ public class ClientIdClientSecretAuthenticationProvider : ITransformerBeeAuthent private readonly bool _useAuthentication; /// - /// + /// /// public bool UseAuthentication() => _useAuthentication; @@ -36,10 +36,10 @@ public class ClientIdClientSecretAuthenticationProvider : ITransformerBeeAuthent /// private readonly SemaphoreSlim _tokenRequestSemaphore = new(1, 1); - /// A that is based on a client secret and a client id + /// A that is based on a client secret and a client id /// OAuth2 client secret (ask Hochfrequenz) /// OAuth2 client ID (ask Hochfrequenz) - public ClientIdClientSecretAuthenticationProvider(string? clientId, string? clientSecret) + public ClientIdClientSecretAuthenticator(string? clientId, string? clientSecret) { if (!string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(clientSecret)) { @@ -97,7 +97,7 @@ private static async Task GetDiscoveryDocument(HttpCl /// /// the token /// if something goes wrong - public async Task GetTokenAsync(HttpClient client) + public async Task Authenticate(HttpClient client) { using (_tokenRequestSemaphore) { diff --git a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs index d74cae7..1dd9eaf 100644 --- a/TransformerBeeClient/TransformerBeeClient/Interfaces.cs +++ b/TransformerBeeClient/TransformerBeeClient/Interfaces.cs @@ -36,7 +36,7 @@ public interface ICanConvertToEdifact /// /// Can provide information on whether you need to authenticate against transformer.bee and how /// -public interface ITransformerBeeAuthenticationProvider +public interface ITransformerBeeAuthenticator { /// /// returns true iff the client should use authentication @@ -48,5 +48,5 @@ public interface ITransformerBeeAuthenticationProvider /// provides the token to authenticate against transformer.bee (if and only if is true) /// /// - public Task GetTokenAsync(HttpClient client); + public Task Authenticate(HttpClient client); } diff --git a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs index 3c97cde..fdf0e02 100644 --- a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs +++ b/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs @@ -1,13 +1,13 @@ namespace TransformerBeeClient; /// -/// a stub that just indicates, we don't need to authenticate against transformer.bee, because we're e.g. in the same network +/// a stub that just indicates, we don't need to authenticate against transformer.bee, because we're e.g. in the same network /// -public class NoAuthenticationProvider : ITransformerBeeAuthenticationProvider +public class NoAuthenticator : ITransformerBeeAuthenticator { public bool UseAuthentication() => false; - public Task GetTokenAsync(HttpClient client) + public Task Authenticate(HttpClient client) { throw new NotImplementedException("This must never be called, because we don't use authentication."); } diff --git a/TransformerBeeClient/TransformerBeeClient/RestClient.cs b/TransformerBeeClient/TransformerBeeClient/RestClient.cs index d9a60df..3628e2b 100644 --- a/TransformerBeeClient/TransformerBeeClient/RestClient.cs +++ b/TransformerBeeClient/TransformerBeeClient/RestClient.cs @@ -14,7 +14,7 @@ namespace TransformerBeeClient; /// public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact { - private readonly ITransformerBeeAuthenticationProvider _authenticationProvider; + private readonly ITransformerBeeAuthenticator _authenticator; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions = new() @@ -28,10 +28,10 @@ public class TransformerBeeRestClient : ICanConvertToBo4e, ICanConvertToEdifact /// It will create a client from said factory and use the for that. /// /// factory to create the http client from - /// something that tells you whether and how you need to authenticate yourself at transformer.bee + /// something that tells you whether and how you need to authenticate yourself at transformer.bee /// name used to create the client /// Find the OpenAPI Spec here: https://transformerstage.utilibee.io/swagger/index.html - public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, ITransformerBeeAuthenticationProvider authenticationProvider, string httpClientName = "TransformerBee") + public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, ITransformerBeeAuthenticator authenticator, string httpClientName = "TransformerBee") { _httpClient = httpClientFactory.CreateClient(httpClientName); if (_httpClient.BaseAddress == null) @@ -39,7 +39,7 @@ public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, ITransform throw new ArgumentNullException(nameof(httpClientFactory), $"The http client factory must provide a base address for the client with name '{httpClientName}'"); } - _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); + _authenticator = authenticator ?? throw new ArgumentNullException(nameof(authenticator)); } /// @@ -47,9 +47,9 @@ public TransformerBeeRestClient(IHttpClientFactory httpClientFactory, ITransform /// private async Task EnsureAuthentication() { - if (_authenticationProvider.UseAuthentication()) + if (_authenticator.UseAuthentication()) { - var token = await _authenticationProvider.GetTokenAsync(_httpClient); + var token = await _authenticator.Authenticate(_httpClient); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } } @@ -108,9 +108,9 @@ public async Task> ConvertToBo4e(string edifact, EdifactFor var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticationProvider.UseAuthentication()) + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticator.UseAuthentication()) { - throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticationProvider)}?"); + throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticator)}?"); } throw new HttpRequestException($"Could not convert {edifact} to BO4E. Status code: {httpResponse.StatusCode}"); @@ -149,9 +149,9 @@ public async Task ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVer var httpResponse = await _httpClient.PostAsync(convertUrl, new StringContent(requestJson, Encoding.UTF8, "application/json")); if (!httpResponse.IsSuccessStatusCode) { - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticationProvider.UseAuthentication()) + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized && !_authenticator.UseAuthentication()) { - throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticationProvider)}?"); + throw new AuthenticationException($"Did you correctly set up the {nameof(ITransformerBeeAuthenticator)}?"); } throw new HttpRequestException($"Could not convert to EDIFACT; Status code: {httpResponse.StatusCode}"); From 4c92dab98c8f631362a56d8f1607b9da99b744ce Mon Sep 17 00:00:00 2001 From: konstantin Date: Fri, 2 Feb 2024 06:54:41 +0000 Subject: [PATCH 12/13] rename --- ...enticationProvider.cs => ClientIdClientSecretAuthenticator.cs} | 0 .../{NoAuthenticationProvider.cs => NoAutenticator.cs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename TransformerBeeClient/TransformerBeeClient/{ClientIdClientSecretAuthenticationProvider.cs => ClientIdClientSecretAuthenticator.cs} (100%) rename TransformerBeeClient/TransformerBeeClient/{NoAuthenticationProvider.cs => NoAutenticator.cs} (100%) diff --git a/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticator.cs similarity index 100% rename from TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticationProvider.cs rename to TransformerBeeClient/TransformerBeeClient/ClientIdClientSecretAuthenticator.cs diff --git a/TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs b/TransformerBeeClient/TransformerBeeClient/NoAutenticator.cs similarity index 100% rename from TransformerBeeClient/TransformerBeeClient/NoAuthenticationProvider.cs rename to TransformerBeeClient/TransformerBeeClient/NoAutenticator.cs From be5fb8adc50c371e51d8f90b45210ebba9c69880 Mon Sep 17 00:00:00 2001 From: konstantin Date: Fri, 2 Feb 2024 06:59:56 +0000 Subject: [PATCH 13/13] wip --- .github/dependabot.yml | 2 +- .github/workflows/integrationtests.yml | 2 +- README.md | 6 +++--- .../EdifactToBo4eTests.cs | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f4dd377..8aa2465 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ version: 2 updates: - package-ecosystem: "nuget" # See documentation for possible values - directory: "/" # Location of package manifests + directory: "/TransformerBeeClient" # Location of package manifests schedule: interval: "weekly" # Maintain dependencies for GitHub Actions diff --git a/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml index 3be05e3..42b38b2 100644 --- a/.github/workflows/integrationtests.yml +++ b/.github/workflows/integrationtests.yml @@ -54,4 +54,4 @@ jobs: CLIENT_ID: ${{ secrets.AUTH0_TEST_CLIENT_ID }} CLIENT_SECRET: ${{ secrets.AUTH0_TEST_CLIENT_SECRET }} run: | - dotnet test + dotnet test --filter TestCollectionName='authenticated' diff --git a/README.md b/README.md index 694c6ac..11d651e 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,17 @@ dotnet add package TransformerBeeClient ``` ### Authentication -You need to provide something that implements `ITransformerBeeAuthenticationProvider` to the `TransformerBeeClient`. +You need to provide something that implements `ITransformerBeeAuthenticator` to the `TransformerBeeClient`. #### No Authentication -If you're hosting transformer.bee in the same network and there is no authentication, you can use the `NoAuthenticationProvider`. +If you're hosting transformer.bee in the same network and there is no authentication, you can use the `NoAuthenticator`. ```csharp using TransformerBeeClient; var myAuthenticator = new NoAuthenticationProvider(); ``` #### OAuth2 Client and Secret -If, which is more likely, Hochfrequenz provided you with a client ID and secret, you can use the `ClientIdClientSecretAuthenticationProvider` class like this: +If, which is more likely, Hochfrequenz provided you with a client ID and secret, you can use the `ClientIdClientSecretAuthenticator` class like this: ```csharp using TransformerBeeClient; var myAuthenticator = new ClientIdClientSecretAuthenticationProvider("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); diff --git a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs index b1c599a..690cb96 100644 --- a/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs +++ b/TransformerBeeClient/TransformerBeeClient.IntegrationTest/EdifactToBo4eTests.cs @@ -36,6 +36,7 @@ public async Task Edifact_Can_Be_Converted_To_Bo4e() /// /// those tests only run if the environment variables are set up correctly /// +[Collection("authenticated")] public class EdifactToBo4eTestsWithAuthentication : IClassFixture { private readonly GithubActionClientFixture _client;