-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider adding Action<IServiceProvider,ClientCredentialsClient> to ClientCredentialsTokenManagementBuilder.AddClient #1494
Comments
Per the documentation Customizing Client Credentials Token Management
The way you implement your ClientCredentialsClientConfigurationOptions class is up to you. Here is how I implemented it.
public class ClientCredentialSettings
{
/// <summary>
/// The discovery url of the token endpoint
/// </summary>
public string? Authority { get; set; }
/// <summary>
/// The client ID
/// </summary>
public string? ClientId { get; set; }
/// <summary>
/// The static (shared) client secret
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// The scope
/// </summary>
public string? Scope { get; set; }
public bool EnableDPoP { get; set; } = false;
}
Dictionary<string, ClientCredentialSettings> dict = new();
builder.Configuration.GetSection("ClientCredentialSettings.ClientCredentialOptions").Bind(dict);
builder.Services.AddSingleton(typeof(IDictionary<string, ClientCredentialSettings>), dict);
DiscoveryCache discoveryCache = new(builder.Configuration.GetValue<string>(IdentitySettings.Authority));
builder.Services.AddSingleton(typeof(DiscoveryCache), discoveryCache);
builder.Services.AddSingleton<IConfigureOptions<ClientCredentialsClient>, BffClientConfigurationOptions>(); BffClientConfigurationOptions: public class BffClientConfigurationOptions : IConfigureNamedOptions<ClientCredentialsClient>
{
const string CacheKeyPrefix = "DistributedDPoPKeyStore";
const char CacheKeySeparator = ':';
private readonly DiscoveryCache _discoveryCache;
private readonly IDictionary<string, ClientCredentialSettings> _dict;
private readonly IDistributedCache _cache;
private readonly ILogger _logger;
public BffClientConfigurationOptions(DiscoveryCache discoveryCache, IDictionary<string, ClientCredentialSettings> dict, IDistributedCache cache, ILoggerFactory loggerFactory)
{
_discoveryCache = discoveryCache;
_dict = dict;
_cache = cache;
_logger = loggerFactory.CreateLogger<BffClientConfigurationOptions>();
}
public void Configure(string? name, ClientCredentialsClient options)
{
ClientCredentialSettings _settings = new();
if (!string.IsNullOrEmpty(name) && _dict.ContainsKey(name) && _dict.First(j => j.Key == name).Value.EnableDPoP)
{
_settings = _dict.First(j => j.Key == name).Value;
//check for key in cache if not there create one and store it
CancellationToken cancellationToken = default;
var cacheKey = GenerateCacheKey(name);
var entry = _cache.GetStringAsync(cacheKey, token: cancellationToken).GetAwaiter().GetResult();
_logger.LogDebug("Cache hit for DPoP nonce for Cleint Name: {clientName}", name);
if (entry is null)
{
entry = CreateDPoPKey();
StoreDPoPKey(name, cacheKey, entry, cancellationToken);
}
options.DPoPJsonWebKey = entry;
}
//var disco = _discoveryCache.GetAsync().GetAwaiter().GetResult();
//options.TokenEndpoint = disco.TokenEndpoint;
options.TokenEndpoint = _settings.Authority;
options.ClientId = _settings.ClientId;
options.ClientSecret = _settings.ClientSecret;
options.Scope = _settings.Scope;
}
public void Configure(ClientCredentialsClient options) => Configure(Options.DefaultName, options);
private void StoreDPoPKey(string clientName, string cacheKey, string data, CancellationToken cancellationToken)
{
var cacheExpiration = DateTimeOffset.UtcNow.AddHours(1);
var entryOptions = new DistributedCacheEntryOptions
{
AbsoluteExpiration = cacheExpiration
};
_logger.LogTrace("Caching DPoP nonce for Client Name: {clientName}. Expiration: {expiration}", clientName, cacheExpiration);
_cache.SetStringAsync(cacheKey, data, entryOptions, token: cancellationToken).GetAwaiter().GetResult();
}
private static string CreateDPoPKey()
{
var key = new RsaSecurityKey(RSA.Create(2048));
var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key);
jwk.Alg = "PS256";
var jwkJson = JsonSerializer.Serialize(jwk);
return jwkJson;
}
protected virtual string GenerateCacheKey(string clientName)
{
return $"{CacheKeyPrefix}{CacheKeySeparator}{clientName}{CacheKeySeparator}{clientName}";
}
} |
Okey and what would be the equivalent for two different named clients? I mean, i have to configure two different clients for two different services, how do I configure it with that approach. For example:
|
So my Clients ("Client-1","Client-2", "Client-3", etc...) would be in some configuration like appsettings.json or preferably app configuration services. The json would look like this: appsettings.json {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
},
"AllowedHosts": "*",
"com.Sample.Bff": {
"ClientCredentialSettings": {
"Client-1": {
"Authority": "https://demo.duendesoftware.com",
"ClientId": "CatalogClient",
"ClientSecret": "secret",
"EnableDPoP": "true",
"Resource": "Catalog",
"Scope": "Catalog.Bff"
},
"Client-2": {
"Authority": "https://demo.duendesoftware.com",
"ClientId": "PaymentClient",
"ClientSecret": "secretp",
"EnableDPoP": "true",
"Resource": "Payment",
"Scope": "Payment.Bff"
},
"Client-3": {
"Authority": "https://demo.duendesoftware.com",
"ClientId": "UrlShortenerClient",
"ClientSecret": "secretu",
"EnableDPoP": "false",
"Resource": "UrlShortener",
"Scope": "UrlShortener.Bff"
}
}
} Add ClientCredentialsTokenManagement : builder
.Services.AddClientCredentialsTokenManagement(); The ClientCredentialSettings section of the configuration will form the IDictionary<string, ClientCredentialSettings> class in the pipeline. Dictionary<string, ClientCredentialSettings> dict = new();
builder.Configuration.GetSection("com.Sample.Bff:ClientCredentialSettings").Bind(dict);
builder.Services.AddSingleton(typeof(IDictionary<string, ClientCredentialSettings>), dict); Next add your HttpClients and attach the tokenHandler based on the client you want to use for that HttpClient: builder.Services.AddHttpClient<CatalogService>(configureClient =>
{
configureClient.BaseAddress = new Uri("https//catalog.exampleapp.com");
configureClient.DefaultRequestHeaders.Add(HeaderNames.Accept, "*/*");
configureClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "catalog client marquee");
})
.AddClientCredentialsTokenHandler("Client-1")
;
builder.Services.AddHttpClient<PaymentService>(configureClient =>
{
configureClient.BaseAddress = new Uri("https//payment.exampleapp.com");
configureClient.DefaultRequestHeaders.Add(HeaderNames.Accept, "*/*");
configureClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "payment client");
})
.AddClientCredentialsTokenHandler("Client-2")
; When the AddClientCredentialsTokenHandler resolves, it will create a client based on the entry "Client-1". It goes without saying that if you try to create AddClientCredentialsTokenHandler("Client-7") and you do not have and entry where the key value is "Client-7" it will fail. So finally now my CatalogService and PaymentServices should be all set and can call the client without having to think about authentication and authorization. public class CatalogService
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
public CatalogService(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
{
_logger = loggerFactory.CreateLogger<CatalogService>();
_httpClientFactory = httpClientFactory;
}
public async Task Run()
{
var url = "/catalog";
HttpClient client = _httpClientFactory.CreateClient(nameof(CatalogService));
try
{
HttpResponseMessage response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
//So something
}
}
catch (Exception ex)
{
_logger.LogInformation(new EventId(-100000, name: "CatalogService.Error"), ex, "{TimerFunctionName} Processor excuted at {DateOfTImer} ", new object[] { "CatalogService.Error", DateTime.UtcNow });
}
finally
{
client.Dispose();
}
}
} |
@JotaCe14 Is this resolved for you or would you like to add a comment? If there's nothing more to add I'd like to close the issue. |
It's okey for me, thanks! |
Which version of Duende.AccessTokenManagement are you using?
3.0.1
Which version of .NET are you using?
NET 8.0
Describe the bug
When adding clients to AddClientCredentialsTokenManagement() I would like to use some services like IOptions<> to configure
my ClientCredentialsClient properties dynamically.
To Reproduce
Expected behavior
services
.AddClientCredentialsTokenManagement()
.AddClient("token-client", (serviceProvider, client) =>
{
//HERE IS WHERE I WOULD LIKE TO GET SOME SERVICES LIKE:
var service = serviceProvider.GetRequiredService();
var settings = serviceProvider.GetRequiredService<IOptions>().Value;
client.ClientId = settings.ClientId;
client.ClientSecret = settings.ClientSecret;
client.TokenEndpoint = service.GetUrl(CancellationToken.None);
client.Scope = settings .ClientScope;
});
The text was updated successfully, but these errors were encountered: