Skip to content

Commit

Permalink
Refactor to prefer use of IMaskinportenSettings in ServiceCollectionE…
Browse files Browse the repository at this point in the history
…xtensions (#21)

* Refactor DI extensions to use IMaskinportenSettings
* Update helpers and definitions
* Update samples and READMEs
* Minor fixes
  • Loading branch information
elsand authored Apr 13, 2023
1 parent 48a7526 commit 4e394d5
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 73 deletions.
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,40 @@ You will need to configure a client definition, which is a way of providing the
Here is an example with a both a [named](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0#named-clients) and [typed](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0#typed-clients) client using a client definition where the secret is a private RSA key in a JWK supplied in the injected settings.

1. Client needs to configured in `ConfigureServices`, where `services` is a `IServiceCollection`
1. Client needs to configured in `ConfigureServices`, where `services` is a `IServiceCollection`. Configuration settings for the client is provided as a instance of `MaskinportenSettings` (or any instance implementing `IMaskinportenSettings`). Alternatively, one can pass a `IConfiguration` instance to an overload that will bind this to a `MaskinportenSettings` instance.

```c#

// We assume `Configuration` is a IConfiguration instance containing the settings from eg. appsettings.json.
var maskinportenSettings = new MaskinportenSettings();
Configuration.GetSection("MaskinportenSettings").Bind(maskinportenSettings);

// Named client
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition>("myhttpclient",
Configuration.GetSection("MaskinportenSettings"));
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition>("myhttpclient", maskinportenSettings);

// For convenience you can pass the IConfiguration instance directly
// services.AddMaskinportenHttpClient<SettingsJwkClientDefinition>("myhttpclient", Configuration.GetSection("MaskinportenSettings"));
// Typed client (MyMaskinportenHttpClient is any class accepting a HttpClient paramter in its constructor)
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettings"));
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(maskinportenSettings);

// Another typed client, using the same app settings, but overriding the setting for Altinn token exchange
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettings"), clientDefinition =>
maskinportenSettings, clientDefinition =>
{
clientDefinition.ClientSettings.ExhangeToAltinnToken = true;
});

// You can chain additional handlers or configure the client if required
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettings"))
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(maskinportenSettings)
.AddHttpMessageHandler(sp => ...)
.ConfigureHttpClient(client => ...)

// Registering av Maskinporten-powered client without adding it to HttpClientFactory / DIC
services.RegisterMaskinportenClientDefinition<SettingsJwkClientDefinition>(
"my-client-definition-instance-key",
Configuration.GetSection("MaskinportenSettings"));
maskinportenSettings);

// This can then be added as a HttpMessageHandler to any IClientBuilder. This is
// useful if you're already using a client builder (DAN, Polly, Refit etc).
Expand Down Expand Up @@ -119,7 +125,7 @@ public class MyController : ControllerBase

<details><summary>See examples using the various client definition types</summary>

Below are usage examples. Note that configuration binding (ie. `Configuration.GetSection("somesection")`) is omitted, as are the three mandatory settings that must always be supplied (Environment, ClientId, Scope)
Below are usage examples.

### SettingsJwk

Expand Down Expand Up @@ -175,6 +181,46 @@ services.AddMaskinportenHttpClient<Pkcs12ClientDefinition>( ... )
```
</details>

## Custom client definitions
If you need to fetch the secret from some other source, you can provide your own implementation of `IClientDefinition` and pass it a custom `IMaskinportenSettings` instance containing any additional settings your source requires

```c#
// ---- MyExtendedMaskinportenSettings.cs ----
public class MyExtendedMaskinportenSettings : MaskinportenSettings
{ // Extends MaskinportenSettings to avoid having to specify every field in IMaskinportenSettings
public string MyCustomSetting { get; set; }
}

// ---- MyCustomClientDefinition.cs ----
public class MyCustomClientDefinition : IClientDefinition
{
public IMaskinportenSettings ClientSettings { get; set; }

// The custom client definitions are registered in the DIC as singletons
public MyCustomClientDefinition(ILogger<MyCustomClientDefinition> logger)
{
_logger = logger;
}

public async Task<ClientSecrets> GetClientSecrets()
{
var myExtendedSettings = (MyExtendedMaskinportenSettings)ClientSettings;
// use myExtendedSettings.MyCustomSetting
...
}
}

// ---- Program.cs / Startup.cs ----
var myExtendedMaskinportenSettings = new MyExtendedMaskinportenSettings();
// We assume `Configuration` is a IConfiguration instance containing the settings from eg. appsettings.json
// and that `services` is a IServiceCollection
Configuration.GetSection("ExtendedMaskinportenSettings").Bind(myExtendedMaskinportenSettings);

// Named client using a custom client definition and settings
services.AddMaskinportenHttpClient<MyCustomClientDefinition>("myhttpclient", myExtendedMaskinportenSettings);
```

## Using with Azure Keyvault as configuration provider i Azure App Services

JWKs or certificates can be injected into application settings for Azure App Services or Azure Functions using [key vault references](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli). This can then be easily used with the `SettingsJwk` or `SettingsX509` client definitions.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Altinn.ApiClients.Maskinporten.Config;
using Altinn.ApiClients.Maskinporten.Config;

namespace SampleWebApp.Config
{
Expand All @@ -11,4 +7,7 @@ public class MyCustomClientDefinitionSettings : MaskinportenSettings
public string AzureKeyVaultName { get; set; }
public string SecretName { get; set; }
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ public async Task<string> Get()
var client0 = _clientFactory.CreateClient("myhttpclient");
var result0 = await client0.GetAsync(url + "?myhttpclient");

var client1 = _clientFactory.CreateClient("myotherhttpclient");
/*var client1 = _clientFactory.CreateClient("myotherhttpclient");
var result1 = await client1.GetAsync(url + "?myotherhttpclient");
// Perform some requests with both the named client and the type client. This will both use the same token.
var result2 = await _myMaskinportenHttpClient.PerformStuff(url + "?_myMaskinportenHttpClient");
var result3 = await _myOtherMaskinportenHttpClient.PerformStuff(url + "?_myOtherMaskinportenHttpClient");
var result4 = await _myThirdMaskinportenHttpClient.PerformStuff(url + "?_myThirdMaskinportenHttpClient");
var result5 = await _myFourthMaskinportenHttpClient.PerformStuff(url + "?_myFourthMaskinportenHttpClient");
var result5 = await _myFourthMaskinportenHttpClient.PerformStuff(url + "?_myFourthMaskinportenHttpClient");*/

return "Done!";
}
Expand Down
12 changes: 6 additions & 6 deletions samples/SampleWebApp/MyCustomClientDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ namespace SampleWebApp
public class MyCustomClientDefinition : IClientDefinition
{
private readonly ILogger<MyCustomClientDefinition> _logger;
public MaskinportenSettings ClientSettings { get; set; }
public MyCustomClientDefinitionSettings MyCustomClientDefinitionSettings { get; set; } = new();

public IMaskinportenSettings ClientSettings { get; set; }
private ClientSecrets _clientSecrets;

public MyCustomClientDefinition(ILogger<MyCustomClientDefinition> logger)
Expand All @@ -32,13 +30,15 @@ public async Task<ClientSecrets> GetClientSecrets()
}

_logger.LogInformation("Getting secrets from Azure");

var myCustomClientDefinitionSettings = (MyCustomClientDefinitionSettings)ClientSettings;

var secretClient = new SecretClient(
new Uri($"https://{MyCustomClientDefinitionSettings.AzureKeyVaultName}.vault.azure.net/"),
new Uri($"https://{myCustomClientDefinitionSettings.AzureKeyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());

var secret = await secretClient.GetSecretAsync(MyCustomClientDefinitionSettings.SecretName);
var base64Str = secret.Value.ToString();
var secret = await secretClient.GetSecretAsync(myCustomClientDefinitionSettings.SecretName);
var base64Str = secret.HasValue ? secret.Value.Value : null;
if (base64Str == null)
{
throw new ApplicationException("Unable to fetch cert from key vault");
Expand Down
43 changes: 22 additions & 21 deletions samples/SampleWebApp/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Altinn.ApiClients.Maskinporten.Config;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
Expand All @@ -6,6 +7,7 @@
using Altinn.ApiClients.Maskinporten.Interfaces;
using Altinn.ApiClients.Maskinporten.Services;
using Altinn.ApiClients.Maskinporten.Extensions;
using SampleWebApp.Config;

namespace SampleWebApp
{
Expand All @@ -31,53 +33,52 @@ public void ConfigureServices(IServiceCollection services)
// will be used.
services.AddSingleton<ITokenCacheProvider, FileTokenCacheProvider>();


// Using a typed HttpClient is the preferred way of setting up a MaskinportenHttpClient;
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettingsForSomeExternalApi"));

var maskinportenSettingsForSomeExternalApi = new MaskinportenSettings();
Configuration.GetSection("MaskinportenSettingsForSomeExternalApi").Bind(maskinportenSettingsForSomeExternalApi);
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(maskinportenSettingsForSomeExternalApi);

// If you need to access multiple APIs requiring different settings (ie. scopes) you must supply a different
// typed client (may inherit a common base client)
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyOtherMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettingsForSomeOtherExternalApi"));
var maskinportenSettingsForSomeOtherExternalApi = new MaskinportenSettings();
Configuration.GetSection("MaskinportenSettingsForSomeOtherExternalApi").Bind(maskinportenSettingsForSomeOtherExternalApi);
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyOtherMaskinportenHttpClient>(maskinportenSettingsForSomeOtherExternalApi);

// You can reuse application settings for the across different HTTP clients, but override specific settings
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyThirdMaskinportenHttpClient>(
Configuration.GetSection("MaskinportenSettingsForSomeOtherExternalApi"), clientDefinition =>
maskinportenSettingsForSomeOtherExternalApi, clientDefinition =>
{
clientDefinition.ClientSettings.ExhangeToAltinnToken = true;
clientDefinition.ClientSettings.Scope =
"altinn:serviceowner/instances.read altinn:serviceowner/instances.write";
});

// As an alternative, named HTTP clients can be used.
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition>("myhttpclient", maskinportenSettingsForSomeExternalApi);

// Overloads are provided to send in a IConfiguration instance directly. This will be bound to an instance of MaskinportenSettings.
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition>("myhttpclient",
Configuration.GetSection("MaskinportenSettingsForSomeExternalApi"));

// You can also define your own client definitions:
services.AddMaskinportenHttpClient<MyCustomClientDefinition, MyFourthMaskinportenHttpClient>(
Configuration.GetSection("MyCustomClientDefinition"), clientDefinition =>
{
// Any additional custom settings and/or fields in your custom client definition should be populated in the configureClientDefinition delegate
clientDefinition.MyCustomClientDefinitionSettings = new MyCustomClientDefinitionSettings();
Configuration.GetSection("MyCustomClientDefinition").Bind(clientDefinition.MyCustomClientDefinitionSettings);
});

// You can also define your own client definitions with custom settings, which should inherit MaskinportenSettings (or implement IMaskinportenSettings)
var myCustomClientDefinitionSettings = new MyCustomClientDefinitionSettings();
Configuration.GetSection("MyCustomClientDefinition").Bind(myCustomClientDefinitionSettings);
services.AddMaskinportenHttpClient<MyCustomClientDefinition, MyFourthMaskinportenHttpClient>(myCustomClientDefinitionSettings);

// Named http clients for custom client definitions
services.AddMaskinportenHttpClient<MyCustomClientDefinition>("myotherhttpclient", Configuration.GetSection("MyCustomClientDefinition"), clientDefinition =>
{
Configuration.GetSection("MyCustomClientDefinition").Bind(clientDefinition.MyCustomClientDefinitionSettings);
});
services.AddMaskinportenHttpClient<MyCustomClientDefinition>("myotherhttpclient", myCustomClientDefinitionSettings);

// You can chain additional handlers or configure the client further if you want
/*
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(Configuration.GetSection("MaskinportenSettingsForSomeExternalApi"))
services.AddMaskinportenHttpClient<SettingsJwkClientDefinition, MyMaskinportenHttpClient>(maskinportenSettingsForSomeExternalApi)
.AddHttpMessageHandler(sp => ...)
.ConfigureHttpClient(client => ...)
*/

/*
// Register a client definition and a configuration, identified by some arbitrary string key
services.RegisterMaskinportenClientDefinition<SettingsJwkClientDefinition>("my-client-definition-key", Configuration.GetSection("MaskinportenSettingsForSomeExternalApi"));
services.RegisterMaskinportenClientDefinition<SettingsJwkClientDefinition>("my-client-definition-key", maskinportenSettingsForSomeExternalApi);
// This can then be added as a HttpMessageHandler to any IClientBuilder (if also using DAN, Polly, Refit etc)
services.AddHttpClient<MyMaskinportenHttpClient>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Altinn.ApiClients.Maskinporten.Config
{
public class MaskinportenSettings
public class MaskinportenSettings : IMaskinportenSettings
{
/// <summary>
/// ClientID to use
Expand Down
Loading

0 comments on commit 4e394d5

Please sign in to comment.