Reputation: 409
This feels like a simple problem to solve, but in all my searching I'm still yet to find a solution that works for me. Could be another case of not finding what I'm after as I'm not searching the right 'thing', but here we are..
I have a C# Web API program where I want to configure the kestrel server from a config object.
I receive this config into my service via rest call, into a CustomConfig
object. I can get this config object either in Program.cs
or in Startup.cs
, but since I don't want to repeat myself and make additional calls, I don't want to do this in both places.
My preference is to get the config in Startup.cs
since that's where the rest of my configuration code sits, and is where I'm already using my CustomConfig
object. However, I can't find a way to configure the kestrel server to use the certificate I'm giving it (in Startup.cs), nor can I see a way to inject this config into Startup.cs
from Program.cs
.
In other projects I have passed the location of the PFX file as an environment variable: ASPNETCORE_Kestrel__Certificates__Default__Path
(in which case everything works without additional code config), but in this project all config must be retrieved via rest call, so this is not an option here.
I currently have everything running, but only by making the rest call to get config twice. The current implementation to configure kestrel is storing the PFX in CustomConfig
as a base64 string, and configuring in Program.cs
:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
CustomConfig config = CustomConfig() // <- I receive config here
webBuilder.UseStartup<Startup>();
webBuilder.UseKestrel(options =>
{
options.ConfigureHttpsDefaults(d =>
{
byte[] pfxBytes = Convert.FromBase64String(config.Base64PFXBytes);
d.ServerCertificate = new X509Certificate2(pfxBytes, "sslKey");
});
});
});
}
CustomConfig
object which is used to configure services in Startup.cs
CustomConfig
So I'm looking for help to either:
Startup.cs
CustomConfig
object from Program.cs
into Startup.cs
Hopefully that makes sense.. Welcome any & all solutions / additional questions for clarity!
Thanks in advance!
Upvotes: 3
Views: 1648
Reputation: 11081
ASP.NET Core abstracts configuration using IConfiguration
interface. Without going into details, it collects
configurations from various IConfigurationSource
and layers them on top of each other, which lets us override a
setting that's defined in one source in another source by defining it with the same key.
IConfigurationSource
Let's implement an IConfigurationSource
. We can use ConfigurationSource
abstract class as our starting point. We'll
use an in-memory implementation, then switch to a remote source.
class RemoteConfigurationSource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RemoteConfigurationProvider(_options);
}
private class RemoteConfigurationProvider : ConfigurationProvider
{
public override void Load()
{
// TODO: fetch data from the API
var remoteConfig = new Dictionary<string, string>
{
{ "CertificateOptions:PfxBase64", "MIIKkQIBAz....gfQ" },
{ "CertificateOptions:Password", "secret" },
};
Data = remoteConfig;
}
}
}
Then add this to the configuration builder in the ConfigureHostConfiguration
callback:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(builder =>
{
// add new source
builder.AddRemoteConfiguration();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().UseKestrel((context, options) =>
{
var certificateOptions = context.Configuration
.GetSection(KestrelCertificateOptions.ConfigurationKey)
.Get<KestrelCertificateOptions>();
options.ConfigureHttpsDefaults(adapterOptions =>
adapterOptions.ServerCertificate = certificateOptions.Certificate);
});
});
}
public static class ConfigurationBuilderExtensions
{
public static IConfigurationBuilder AddRemoteConfiguration(this IConfigurationBuilder builder) =>
builder.Add(new RemoteConfigurationSource());
}
class KestrelCertificateOptions
{
public const string ConfigurationKey = "CertificateOptions";
public string PfxBase64 { get; set; }
public string Password { get; set; }
public X509Certificate2 Certificate => new X509Certificate2(Convert.FromBase64String(PfxBase64), Password);
}
When we run the app, ASP.NET Core will load and use our in-memory configuration.
Now let's fetch the config from a remote API. It needs to return configuration values with sections names delimited with
with a colon :
. Here's the same config as JSON, filed under CertificateOptions
section:
{
"CertificateOptions:PfxBase64": "MII....oCAgfQ",
"CertificateOptions:Password": "secret"
}
Assume the API wraps returns this data wrapped as:
{
"Application": "MyApp",
"LastChanged": "2021-08-09 14:38:00",
"Data": {
"CertificateOptions:PfxBase64": "MIIK...oCAgfQ",
"CertificateOptions:Password": "secret"
}
}
so we need to take only Data
key into account when fetching the data.
class RemoteConfigurationSource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RemoteConfigurationProvider();
}
private class RemoteConfigurationProvider : ConfigurationProvider
{
public override void Load()
{
// We cannot await this method, so have to do sync-over-async.
// Not an issue, because it's a one-time thing.
var result = LoadRemoteConfig().GetAwaiter().GetResult();
Data = result.Data;
}
private async Task<RemoteConfigResult> LoadRemoteConfig()
{
// We cannot use IHttpClientFactory here, since ServiceProvider isn't even built yet.
using var httpClient = new HttpClient();
// ... add headers, token to request
return await httpClient.GetFromJsonAsync<RemoteConfigResult>("https://example.com/path/to/json");
}
}
private class RemoteConfigResult
{
public Dictionary<string, string> Data { get; set; }
}
}
To clean things up a bit, we can move the URL and other credentials to appsettings.json:
{
"Logging": {
/*...*/
},
"RemoteConfiguration": {
"Url": "https://jsonkeeper.com/b/B78I",
"ApplicationId": "myconfigappid",
"Secret": "myconfigapisecret"
}
}
Then build a temporary IConfiguration
add as many sources as necessary, then fetch these values:
// Read credentials from appsettings.json
var remoteConfigurationOptions = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build()
.GetSection(RemoteConfigurationOptions.ConfigurationKey)
.Get<RemoteConfigurationOptions>();
public class RemoteConfigurationOptions
{
public const string ConfigurationKey = "RemoteConfiguration";
public string Url { get; set; }
public string ApplicationId { get; set; }
public string Secret { get; set; }
}
Then pass this object to our configuration source, which in turns passes it down to the configuration provider
Upvotes: 6