Client libraries
The other side of the protocol: applications that obtain tokens and send them to APIs. Two small packages cover the
cases a platform has — a service calling another as itself, and a web application calling APIs as its signed-in user.
Neither depends on this server; any standards-conforming authorization server will do. The code on this page is
included from samples/ClientLibraries,
a back end for a front end (BFF) that signs in against the getting started server.
Service to service: client credentials
dotnet add package Fdyay.AccessTokenManagement
// 1. The machinery, and one client registration per outbound hop.
services.AddClientCredentialsTokenManagement()
.AddClient(OrdersSync, client =>
{
client.TokenEndpoint = new Uri(configuration["Authority"] + "/connect/token");
client.ClientId = ClientId.Parse("orders-sync");
client.ClientSecret = ClientSecret.Parse(configuration["OrdersSync:ClientSecret"]!);
client.Scope = Scope.Parse("orders.read");
});
// 2. The HttpClient that carries the token.
services.AddHttpClient<OrdersApiClient>(http => http.BaseAddress = new Uri(configuration["OrdersApi"]!))
.AddClientCredentialsTokenHandler(OrdersSync);
AddClientCredentialsTokenManagement
registers the token service, AddClient describes one client (a
ClientCredentialsClient), and AddClientCredentialsTokenHandler attaches its token
to everything an HttpClient sends. Register one client per hop, each with only the scope that hop needs.
/// <summary>
/// An ordinary typed HttpClient. It never mentions a token: the handler added below obtains one, caches it until
/// shortly before it expires, attaches it, and retries once with a fresh one if the API answers 401.
/// </summary>
public sealed class OrdersApiClient(HttpClient http)
{
/// <summary>The orders changed since <paramref name="since"/>.</summary>
public async Task<IReadOnlyList<OrderSummary>> GetChangedOrdersAsync(DateTimeOffset since, CancellationToken ct)
{
var orders = await http.GetFromJsonAsync<List<OrderSummary>>(
new Uri($"orders?changedSince={since:O}", UriKind.Relative), ct);
return orders ?? [];
}
}
/// <summary>One order, as the orders API lists it.</summary>
public sealed record OrderSummary(string Id, string Status);
The client code never sees a token. The handler gets one on the first request, reuses it until a minute before it expires, lets one caller fetch while the others wait, and retries once with a fresh token when the API answers 401. IClientCredentialsTokenService is there for anything that needs the token itself.
// A call the BFF makes as itself rather than as the user: OrdersApiClient attaches its own token.
app.MapPost("/api/sync", async (OrdersApiClient orders, CancellationToken ct) =>
Results.Ok(await orders.GetChangedOrdersAsync(DateTimeOffset.UtcNow.AddHours(-1), ct)))
.RequireAuthorization();
A BFF calling APIs as its user
dotnet add package Fdyay.AccessTokenManagement.OpenIdConnect
// The BFF signs its users in with OpenID Connect and keeps their tokens in its session cookie.
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Authority"];
options.ClientId = "web";
options.ClientSecret = builder.Configuration["Web:ClientSecret"];
options.ResponseType = "code";
options.SaveTokens = true; // the access and refresh tokens go into the cookie
options.Scope.Add("api");
options.Scope.Add("offline_access"); // a refresh token, so the access token can be renewed
options.MapInboundClaims = false;
});
// Reads the saved access token and, when it is about to expire, refreshes it with the saved refresh token and
// writes both back into the cookie. No options: everything comes from the OpenID Connect scheme above.
builder.Services.AddOpenIdConnectAccessTokenManagement();
The application signs its users in with ASP.NET Core's OpenID Connect handler and keeps their tokens in its cookie
(SaveTokens). AddOpenIdConnectAccessTokenManagement
needs no options: the token endpoint, client id, secret and back-channel HttpClient all come from the OpenID Connect
scheme. This is the pattern the platform's hub Shell uses.
// The browser calls the BFF with its cookie; the BFF calls the API with the user's access token.
app.MapGet("/api/orders", async (HttpContext context, IHttpClientFactory httpClients) =>
{
// Refreshed first if it expires within a minute. GetToken() throws when there is none to be had —
// no session, or a refresh the server refused — so the caller can turn that into a sign-in.
var userToken = await context.GetUserAccessTokenAsync().GetToken();
using var request = new HttpRequestMessage(HttpMethod.Get, "orders");
request.Headers.Authorization = new AuthenticationHeaderValue(userToken.TokenType, userToken.AccessToken);
using var response = await httpClients.CreateClient("api").SendAsync(request, context.RequestAborted);
return Results.Content(
await response.Content.ReadAsStringAsync(context.RequestAborted),
"application/json",
statusCode: (int)response.StatusCode);
})
.RequireAuthorization();
GetUserAccessTokenAsync returns
the session's access token, refreshing it first with the session's refresh token when it expires within a minute and
writing the new tokens back into the cookie. Concurrent requests from one session share a single refresh, so a
one-time-use refresh token is never redeemed twice. A failure is a result, not an exception, until GetToken() makes
it one.