Web Applications
The Duende.AccessTokenManagement
library automates all the tasks around access token lifetime management for
user-centric web applications.
To use this library, start by adding the library to your .NET projects.
dotnet add package Duende.AccessTokenManagement
While many of the details can be customized, by default the following is assumed:
- ASP.NET Core web application
- cookie authentication handler for session management
- OpenID Connect authentication handler for authentication and access token requests against an OpenID Connect compliant token service
- the token service returns a refresh token
By default, the token management library will use the ASP.NET Core default authentication scheme for token storage (this is typically the cookie handler and its authentication session), and the default challenge scheme for deriving token client configuration for refreshing tokens or requesting client credential tokens (this is typically the OpenID Connect handler pointing to your trusted authority).
// setting up default schemes and handlersbuilder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "web";
// automatically revoke refresh token at signout time options.Events.OnSigningOut = async e => { await e.HttpContext.RevokeRefreshTokenAsync(); }; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://sts.company.com";
options.ClientId = "webapp"; options.ClientSecret = "secret";
options.ResponseType = "code"; options.ResponseMode = "query";
options.Scope.Clear();
// OIDC related scopes options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email");
// API scopes options.Scope.Add("invoice"); options.Scope.Add("customer");
// requests a refresh token options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false;
// important! this store the access and refresh token in the authentication session // this is needed to the standard token store to manage the artefacts options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; });
// adds services for token managementbuilder.Services.AddOpenIdConnectAccessTokenManagement();
HTTP Client Factory
Section titled “HTTP Client Factory”Similar to the worker service support, you can register HTTP clients that automatically send the access token of the current user when making API calls. The message handler plumbing associated with those HTTP clients will try to make sure, the access token is always valid and not expired.
// registers HTTP client that uses the managed user access tokenbuilder.Services.AddUserAccessTokenHttpClient("invoices", configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); });
This could be also a typed client:
// registers a typed HTTP client with token management supportbuilder.Services.AddHttpClient<InvoiceClient>(client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }) .AddUserAccessTokenHandler();
Of course, the ASP.NET Core web application host could also do machine to machine API calls that are independent of a user. In this case all the token client configuration can be inferred from the OpenID Connect handler configuration. The following registers an HTTP client that uses a client credentials token for outgoing calls:
// registers HTTP client that uses the managed client access tokenbuilder.Services.AddClientAccessTokenHttpClient("masterdata.client", configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/masterdata/"); });
As a typed client:
builder.Services.AddHttpClient<MasterDataClient>(client => { client.BaseAddress = new Uri("https://api.company.com/masterdata/"); }) .AddClientAccessTokenHandler();
There are three ways to interact with the token management service:
- Manually
- HTTP context extension methods
- HTTP client factory
Manually
Section titled “Manually”You can get the current user and client access token manually by writing code against the IUserTokenManagementService
.
public class HomeController : Controller{ private readonly IHttpClientFactory _httpClientFactory; private readonly IUserTokenManagementService _tokenManagementService;
public HomeController(IHttpClientFactory httpClientFactory, IUserTokenManagementService tokenManagementService) { _httpClientFactory = httpClientFactory; _tokenManagementService = tokenManagementService; }
public async Task<IActionResult> CallApi() { var token = await _tokenManagementService.GetAccessTokenAsync(User); var client = _httpClientFactory.CreateClient(); client.SetBearerToken(token.Value);
var response = await client.GetAsync("https://api.company.com/invoices");
// rest omitted }}
HTTP Context Extension Methods
Section titled “HTTP Context Extension Methods”There are three extension methods on the HTTP context that simplify interaction with the token management service:
GetUserAccessTokenAsync
- returns an access token representing the user. If the current access token is expired, it will be refreshed.GetClientAccessTokenAsync
- returns an access token representing the client. If the current access token is expired, a new one will be requestedRevokeRefreshTokenAsync
- revokes the refresh token
public async Task<IActionResult> CallApi(){ var token = await HttpContext.GetUserAccessTokenAsync(); var client = _httpClientFactory.CreateClient(); client.SetBearerToken(token.Value);
var response = await client.GetAsync("https://api.company.com/invoices");
// rest omitted}
HTTP Client Factory
Section titled “HTTP Client Factory”Last but not least, if you registered clients with the factory, you can use them. They will try to make sure that a current access token is always sent along. If that is not possible, ultimately a 401 will be returned to the calling code.
public async Task<IActionResult> CallApi(){ var client = _httpClientFactory.CreateClient("invoices");
var response = await client.GetAsync("list");
// rest omitted}
…or for a typed client:
public async Task<IActionResult> CallApi([FromServices] InvoiceClient client){ var response = await client.GetList();
// rest omitted}