-
Notifications
You must be signed in to change notification settings - Fork 68
Worker Applications
Important
Documentation for Duende.AccessTokenManagement has moved here.
A common scenario in worker applications or background tasks (or really any demon-style applications) is to call APIs using an OAuth token obtained via the client credentials flow.
The access tokens need to be requested and cached (either locally or shared between multiple instances) and made available to the code calling the APIs. In case of expiration (or other token invalidation reasons), a new access token needs to be requested.
The actual business code should not need to be aware of any of this.
Have a look for the Worker
project in the samples folder for running code.
Start by adding a reference to the Duende.AccessTokenManagement
Nuget package to your application.
You can add the necessary services to the DI system by calling AddClientCredentialsTokenManagement()
. After that you can add one or more named client definitions by calling AddClient
.
// default cache
services.AddDistributedMemoryCache();
services.AddClientCredentialsTokenManagement()
.AddClient("catalog.client", client =>
{
client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token";
client.ClientId = "6f59b670-990f-4ef7-856f-0dd584ed1fac";
client.ClientSecret = "d0c17c6a-ba47-4654-a874-f6d576cdf799";
client.Scope = "catalog inventory";
})
.AddClient("invoice.client", client =>
{
client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token";
client.ClientId = "ff8ac57f-5ade-47f1-b8cd-4c2424672351";
client.ClientSecret = "4dbbf8ec-d62a-4639-b0db-aa5357a0cf46";
client.Scope = "invoice customers";
});
You can register HTTP clients with the factory that will automatically use the above client definitions to request and use access tokens.
The following code registers an HTTP client called invoices
to automatically use the invoice.client
definition:
services.AddClientCredentialsHttpClient("invoices", "invoice.client", client =>
{
client.BaseAddress = new Uri("https://apis.company.com/invoice/");
});
You can also setup a typed HTTP client to use a token client definition, e.g.:
services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://apis.company.com/catalog/");
})
.AddClientCredentialsTokenHandler("catalog.client");
There are two fundamental ways to interact with token management - manually, or via the HTTP factory.
You can retrieve the current access token for a given token client via IClientCredentialsTokenManagementService.GetAccessTokenAsync
.
public class WorkerManual : BackgroundService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IClientCredentialsTokenManagementService _tokenManagementService;
public WorkerManualIHttpClientFactory factory, IClientCredentialsTokenManagementService tokenManagementService)
{
_clientFactory = factory;
_tokenManagementService = tokenManagementService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var client = _clientFactory.CreateClient();
client.BaseAddress = new Uri("https://apis.company.com/catalog/");
// get access token for client and set on HttpClient
var token = await _tokenManagementService.GetAccessTokenAsync("catalog.client");
client.SetBearerToken(token.Value);
var response = await client.GetAsync("list", stoppingToken);
// rest omitted
}
}
}
You can customize some of the per-request parameters by passing in an instance of ClientCredentialsTokenRequestParameters
. This allows forcing a fresh token request (even if a cached token would exist) and also allows setting a per request scope, resource and client assertion.
If you have setup HTTP clients in the HTTP factory, then no token related code is needed at all, e.g.:
public class WorkerHttpClient : BackgroundService
{
private readonly ILogger<WorkerHttpClient> _logger;
private readonly IHttpClientFactory _clientFactory;
public WorkerHttpClient(ILogger<WorkerHttpClient> logger, IHttpClientFactory factory)
{
_logger = logger;
_clientFactory = factory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var client = _clientFactory.CreateClient("invoices");
var response = await client.GetAsync("test", stoppingToken);
// rest omitted
}
}
}
remark The clients in the factory have a message handler attached to them that automatically re-tries the request in case of a 401 response code. The request get re-sent with a newly requested access token. If this still results in a 401, the response is returned to the caller.