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
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/integrationtests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 --filter TestCollectionName='authenticated'
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +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 `ITransformerBeeAuthenticator` to the `TransformerBeeClient`.

Then, you can use the client like this:
#### No Authentication
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 `ClientIdClientSecretAuthenticator` class like this:
```csharp
using TransformerBeeClient;
var myAuthenticator = new ClientIdClientSecretAuthenticationProvider("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET");
```

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

das kommt dan in #12

...todo
```csharp

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ namespace TransformerBeeClient.IntegrationTest;
public class Bo4eToEdifactTests : IClassFixture<ClientFixture>
{
private readonly ClientFixture _client;
private readonly ITransformerBeeAuthenticator _authenticator;

public Bo4eToEdifactTests(ClientFixture clientFixture)
{
_client = clientFixture;
_authenticator = clientFixture.Authenticator;
}

[Fact]
public async Task BOneyComb_Can_Be_Converted_To_Edifact()
{
var httpClientFactory = _client.HttpClientFactory;
ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory);
ICanConvertToEdifact client = new TransformerBeeRestClient(httpClientFactory, _authenticator);
var boneyCombString = await File.ReadAllTextAsync("TestEdifacts/FV2310/55001.json");
var deserializerOptions = new JsonSerializerOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class ClientFixture : IClassFixture<ClientFixture>

public readonly ServiceCollection ServiceCollection;

public readonly ITransformerBeeAuthenticator Authenticator;

public ClientFixture()
{
var services = new ServiceCollection();
Expand All @@ -22,5 +24,6 @@ public ClientFixture()
var serviceProvider = services.BuildServiceProvider();
ServiceCollection = services;
HttpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
Authenticator = new NoAuthenticator(); // easy for integration tests with transformer.bee running in docker
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ public class ConnectionTests : IClassFixture<ClientFixture>
{

private readonly ClientFixture _client;
private readonly ITransformerBeeAuthenticator _authenticator;

public ConnectionTests(ClientFixture clientFixture)
{
_client = clientFixture;
_authenticator = clientFixture.Authenticator;
}

[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, _authenticator);
var result = await client.IsAvailable();
result.Should().BeTrue();
}
Expand All @@ -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<IHttpClientFactory>());
var client = new TransformerBeeRestClient(serviceProvider.GetService<IHttpClientFactory>(), _authenticator);
var checkIfIsAvailable = async () => await client.IsAvailable();
await checkIfIsAvailable.Should().ThrowAsync<HttpRequestException>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FluentAssertions;
using TransformerBeeClient.Model;
using Xunit;
using Xunit.Abstractions;

namespace TransformerBeeClient.IntegrationTest;

Expand All @@ -12,19 +13,59 @@ public class EdifactToBo4eTests : IClassFixture<ClientFixture>
{
private readonly ClientFixture _client;

private readonly ITransformerBeeAuthenticator _authenticator;

public EdifactToBo4eTests(ClientFixture clientFixture)
{
_client = clientFixture;
_authenticator = clientFixture.Authenticator;
}

[Fact]
public async Task Edifact_Can_Be_Converted_To_Bo4e()
{
var httpClientFactory = _client.HttpClientFactory;
ICanConvertToBo4e client = new TransformerBeeRestClient(httpClientFactory);
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<List<Marktnachricht>>();
result.Single().Transaktionen.Should().NotBeNullOrEmpty();
}
}

/// <summary>
/// those tests only run if the environment variables are set up correctly
/// </summary>
[Collection("authenticated")]
public class EdifactToBo4eTestsWithAuthentication : IClassFixture<GithubActionClientFixture>
{
private readonly GithubActionClientFixture _client;

private readonly ITransformerBeeAuthenticator? _authenticationProvider;

ITestOutputHelper _output;

public EdifactToBo4eTestsWithAuthentication(GithubActionClientFixture clientFixture, ITestOutputHelper output)
{
_client = clientFixture;
_authenticationProvider = clientFixture.AuthenticationProvider;
_output = output;
}

[SkippableFact]
public async Task Edifact_Can_Be_Converted_To_Bo4e_With_Authentication()
{
if (_authenticationProvider is null)
{
_output.WriteLine("Skipping test because no authentication provider is available");
}
Skip.If(_authenticationProvider is null, "No authentication provider 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<List<Marktnachricht>>();
result.Single().Transaktionen.Should().NotBeNullOrEmpty();
_output.WriteLine("Successfully converted edifact to bo4e - with authentication!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace TransformerBeeClient.IntegrationTest;

/// <summary>
/// A fixture that sets up the http client factory and an injectable service collection.
/// It's thought to be used in the Github action.
/// </summary>
public class GithubActionClientFixture : IClassFixture<GithubActionClientFixture>
{
public readonly IHttpClientFactory? HttpClientFactory;

public readonly ServiceCollection? ServiceCollection;

public readonly ITransformerBeeAuthenticator? AuthenticationProvider;

public GithubActionClientFixture()
{
var clientId = Environment.GetEnvironmentVariable("CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("CLIENT_SECRET");
if (clientSecret is null && clientId is null)
{
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<IHttpClientFactory>();
AuthenticationProvider = new ClientIdClientSecretAuthenticator(clientId: clientId, clientSecret: clientSecret);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHttpClientFactory>());
var instantiateClient = () => new TransformerBeeRestClient(serviceProvider.GetService<IHttpClientFactory>(), new NoAuthenticator());
instantiateClient.Should().Throw<ArgumentNullException>();
}
}
5 changes: 0 additions & 5 deletions TransformerBeeClient/TransformerBeeClient/Class1.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
namespace TransformerBeeClient;

using System.Security.Authentication;
using IdentityModel.Client;
using System.IdentityModel.Tokens.Jwt;

/// <summary>
/// a <see cref="ITransformerBeeAuthenticator"/> that is based on a client secret and a client id
/// </summary>
public class ClientIdClientSecretAuthenticator : ITransformerBeeAuthenticator
{
protected const string Auth0Domain = "https://hochfrequenz.eu.auth0.com";
protected const string TransformerBeeScope = "client:default";
protected const string TransformerAudience = "https://transformer.bee";

/// <summary>
/// JWT token
/// </summary>
private string? _token;

private readonly string? _clientSecret;
private readonly string? _clientId;

/// <summary>
/// true iff the client should use authentication
/// </summary>
private readonly bool _useAuthentication;

/// <summary>
/// <inheritdoc cref="ITransformerBeeAuthenticator.UseAuthentication"/>
/// </summary>
public bool UseAuthentication() => _useAuthentication;

/// <summary>
/// prevents us from requesting multiple tokens at the same time
/// </summary>
private readonly SemaphoreSlim _tokenRequestSemaphore = new(1, 1);

/// <summary>A <see cref="ITransformerBeeAuthenticator"/> that is based on a client secret and a client id</summary>
/// <param name="clientId">OAuth2 client secret (ask Hochfrequenz)</param>
/// <param name="clientSecret">OAuth2 client ID (ask Hochfrequenz)</param>
public ClientIdClientSecretAuthenticator(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;
}
}

/// <summary>
/// returns true if the <see cref="token"/> will expire in the next 5 minutes
/// </summary>
/// <param name="token">jwt</param>
/// <returns>true if the token should be refreshed</returns>
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;
}

/// <summary>
/// gets the oauth2 discovery document from the respective auth0 domain
/// </summary>
/// <returns></returns>
/// <exception cref="AuthenticationException"></exception>
private static async Task<DiscoveryDocumentResponse> 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;
}

/// <summary>
/// fetch a token to talk to the transformer bee
/// </summary>
/// <returns>the token</returns>
/// <exception cref="AuthenticationException">if something goes wrong</exception>
public async Task<string> Authenticate(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;
}
}
18 changes: 18 additions & 0 deletions TransformerBeeClient/TransformerBeeClient/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,21 @@ public interface ICanConvertToEdifact
/// <returns>the edifact as plain string</returns>
public Task<string> ConvertToEdifact(BOneyComb boneyComb, EdifactFormatVersion formatVersion);
}

/// <summary>
/// Can provide information on whether you need to authenticate against transformer.bee and how
/// </summary>
public interface ITransformerBeeAuthenticator
{
/// <summary>
/// returns true iff the client should use authentication
/// </summary>
/// <returns></returns>
public bool UseAuthentication();

/// <summary>
/// provides the token to authenticate against transformer.bee (if and only if <see cref="UseAuthentication"/> is true)
/// </summary>
/// <returns></returns>
public Task<string> Authenticate(HttpClient client);
}
Loading