Table of Contents

Client authentication and Blazor

When your code calls another API, the call must carry a token that this API accepts: the token of the user, a token obtained on behalf of the user, or a token of your application. Arc4u separates the code that gets the token (a token provider) from the code that sends it (an HttpClient handler or a gRPC interceptor), and connects them by name in the configuration. This guide covers calls from ASP.NET Core services, desktop and mobile apps, and Blazor applications. How an API validates the tokens it receives is covered in Server authentication.

What it solves

Without Arc4u, every typed HttpClient needs code to request a token, cache it, refresh it, and decide whether to send the token of the user or the one of the application. With Arc4u, you declare the token in appsettings.json, register the matching token provider, and add a handler to the client. The handler reads the current user from the application context, asks the provider for a token and sets the header.

flowchart LR
    Code["Your code"] --> Client["HttpClient or gRPC client"]
    Client --> Handler["JwtHttpHandler / OAuth2Interceptor"]
    Handler -->|"ProviderId of the named settings"| Provider["Token provider"]
    Provider -->|"token request"| Idp["Identity provider"]
    Handler -->|"Authorization: Bearer ..."| Api["Downstream API"]

Arc4u leaves the rest to .NET and to established libraries: IHttpClientFactory, the gRPC client factory, the ASP.NET Core authentication handlers, the Blazor authentication state, and Duende.IdentityModel.OidcClient for desktop sign-in.

Packages involved

Package Use it for
Arc4u.OAuth2 The token provider contract (ITokenProvider, TokenKeys) and the token cache. All the packages below reference it.
Arc4u.OAuth2.AspNetCore Services: the client credentials, user name and password, and remote secret providers, with AddClientTokens and AddRemoteSecretsAuthentication.
Arc4u.OAuth2.AspNetCore.Authentication Services: the JwtHttpHandler<T> for HttpClient, and the providers that forward the user's token or exchange it on behalf of the user. It references Arc4u.OAuth2.AspNetCore.
Arc4u.gRPC The OAuth2Interceptor<T> for gRPC clients.
Arc4u.OAuth2.Client Client apps: the AppPrincipalFactory that builds the principal from the token, and the user name and password provider.
Arc4u.OAuth2.Client.Authentication Desktop and mobile apps: OpenID Connect sign-in, token provider and HttpClient handler.
Arc4u.OAuth2.Blazor Blazor WebAssembly: token providers, HttpClient handler and principal. It references Arc4u.OAuth2.Client.
Arc4u.OAuth2.AspNetCore.Blazor Blazor server: the principal of Interactive Server components.

Arc4u.OAuth2.Msal is not shipped in Arc4u 9: see MSAL status.

Install

For a service (ASP.NET Core) that calls other APIs:

dotnet add package Arc4u.OAuth2.AspNetCore.Authentication --prerelease
dotnet add package Arc4u.Caching.Memory --prerelease
dotnet add package Arc4u.Serializer.JSon --prerelease
dotnet add package Arc4u.gRPC --prerelease

The cache packages keep the application tokens (see Caching); use another cache kind if you prefer. Add Arc4u.gRPC only for gRPC clients. For a Blazor app, install Arc4u.OAuth2.Blazor in the WebAssembly project and Arc4u.OAuth2.AspNetCore.Blazor in the server project. For a desktop or mobile app, install Arc4u.OAuth2.Client.Authentication.

You need an app registration in your identity provider (Microsoft Entra ID, Keycloak, ...) for each application that requests tokens.

Configuration

Each token is declared by a named entry. The registration methods read these sections and register one set of settings per entry, named after the entry:

Section Read by Settings name
Authentication:ClientTokens:<name> AddClientTokens <name>
Authentication:OnBehalfOf:<name> AddOnBehalfOf <name>
Authentication:RemoteSecrets:<name> AddRemoteSecretsAuthentication <name>
Authentication:OAuth2.Settings AddJwtAuthentication, AddHybridAuthentication (server) OAuth2
Authentication:OpenId.Settings AddOidcAuthentication, AddHybridAuthentication (server) Cookies
Authentication:OAuth2.Settings (WebAssembly app) AddAuthenticationCookie OAuth2
Authentication:OidcClient.Settings AddOidcClientAuthentication OidcClient

AddClientTokens, AddOnBehalfOf, AddRemoteSecretsAuthentication and AddAuthenticationCookie take a sectionName parameter to read another section. The server methods and AddOidcClientAuthentication take an authenticationSectionName instead, and read the paths of their sub-sections from keys of that section (for example OAuth2SettingsSectionPath or OidcClientIdSettingsSectionPath). The keys of each section are described in Token providers, Blazor and Desktop and mobile clients.

appsettings.json

The shortest setup calls an API with the identity of your service (client credentials). The token provider keeps the token in an Arc4u cache, declared in the Caching section (see Caching):

{
  "Caching": {
    "Default": "Volatile",
    "Caches": [
      {
        "Name": "Volatile",
        "Kind": "Memory",
        "IsAutoStart": true,
        "Settings": {
          "SizeLimitInMB": 100
        }
      }
    ]
  },
  "Authentication": {
    "DefaultAuthority": {
      "Url": "https://login.microsoftonline.com/<tenant-id>/v2.0"
    },
    "TokenCache": {
      "CacheName": "Volatile"
    },
    "ClientTokens": {
      "Inventory": {
        "Scenario": "ClientCredentials",
        "Scopes": [ "api://inventory/.default" ],
        "Settings": {
          "ClientId": "<client-id>",
          "ClientSecret": "<client-secret>"
        }
      }
    }
  }
}
Key Type Default Description
Authentication:DefaultAuthority:Url Uri none (required) The identity provider. Its token endpoint is read from its OpenID Connect metadata, unless TokenEndpoint is set.
Authentication:TokenCache:CacheName string none (required by AddTokenCache) The Arc4u cache that keeps the tokens.
Authentication:TokenCache:MaxTime TimeSpan 00:50:00 How long a token stays in the cache at most.
Authentication:ClientTokens:<name>:Scenario string none (required) ClientCredentials requests a token for the application.
Authentication:ClientTokens:<name>:Scopes string[] openid Scopes of the downstream API.
Authentication:ClientTokens:<name>:Settings:ClientId string none (required) Client ID of your service.
Authentication:ClientTokens:<name>:Settings:ClientSecret string none (required) Client secret of your service.

Code

// Program.cs
using Arc4u.Caching;
using Arc4u.Caching.Memory;
using Arc4u.Configuration;
using Arc4u.Dependency;
using Arc4u.OAuth2;
using Arc4u.OAuth2.AspNetCore;
using Arc4u.OAuth2.Extensions;
using Arc4u.OAuth2.Token;
using Arc4u.OAuth2.TokenProvider;
using Arc4u.Security.Principal;
using Arc4u.Serializer;

var builder = WebApplication.CreateBuilder(args);

// Services used by the handler.
builder.Services.AddILogger();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IScopedServiceProviderAccessor, ScopedServiceProviderAccessor>();
builder.Services.AddScoped<IApplicationContext, ApplicationInstanceContext>();

// The token cache: an Arc4u memory cache (see the Caching guide).
builder.Services.AddCacheContext(builder.Configuration);
builder.Services.AddSingleton<IObjectSerialization, JsonSerialization>();
builder.Services.AddMemoryCacheKind();
builder.Services.AddTokenCache(builder.Configuration);
builder.Services.AddSingleton<ICacheHelper, CacheHelper>();
builder.Services.AddSingleton<ITokenCache, ApplicationCache>();

// The token: settings named "Inventory" and the provider they name.
builder.Services.AddDefaultAuthority(builder.Configuration);
builder.Services.AddClientTokens(builder.Configuration);
builder.Services.AddKeyedSingleton<ITokenProvider, ClientCredentialsTokenProvider>(ClientCredentialsTokenProvider.ProviderName);

// The client that sends it.
builder.Services.AddHttpClient<InventoryClient>(client => client.BaseAddress = new Uri("https://inventory.example.com/"))
    .AddHttpMessageHandler(sp => new JwtHttpHandler<InventoryClient>(
        sp,
        sp.GetRequiredService<ILogger<InventoryClient>>(),
        "Inventory"));

var app = builder.Build();
app.Run();

public sealed class InventoryClient(HttpClient httpClient)
{
    public Task<string> GetStockAsync(string sku, CancellationToken cancellationToken)
        => httpClient.GetStringAsync($"stock/{sku}", cancellationToken);
}

Every request of InventoryClient now carries Authorization: Bearer <token>. The token is requested once and reused until one minute before it expires. When you use the configuration based AddJwtAuthentication of Server authentication, it already calls AddClientTokens, and AddTokenCache when the Authentication:TokenCache section exists.

Common scenarios

Choose the token to send

The downstream API accepts Use Page
The token your API received The Bootstrap provider with the OAuth2 settings Forward the user's token
The user's token of your web app The Oidc provider with the Cookies settings Forward the user's token
A token for the user, with its own audience On-behalf-of Call an API on behalf of the user
A token of your application Client credentials Call an API with the application identity
A technical account User name and password Call an API with a user name and password
An Arc4u remote secret Remote secret Send a remote secret

Call a gRPC service

Add the OAuth2Interceptor<T> to the gRPC client with the same settings. See Add the interceptor to a gRPC client.

Send different tokens depending on the caller

Chain several handlers on one client: see Chain several handlers.

Call an API from a background job

Give the handlers a scope to read the application context from: see Call an API outside a request.

Call an API from a Blazor WebAssembly app

The WebAssembly app gets the user's token from its server with the authentication cookie: see Blazor.

Sign in the user of a desktop or mobile app

See Desktop and mobile clients.

Extensibility points

Service Default implementation Replace it to
ITokenProvider (keyed) The providers listed in Token providers Get tokens another way. Register yours under a new key and put that key in ProviderId.
IClientTokenScenario ClientCredentials, UserPassword, Basic Add a scenario to Authentication:ClientTokens with ClientTokensExtension.SetScenarioResolver.
ITokenCache ApplicationCache Store the application tokens elsewhere.
ISecureCache (client apps) CacheTokens (desktop), SecureCache (Blazor) Encrypt the tokens kept by a desktop app.

See Write your own token provider.

Troubleshooting

The downstream API returns 401

The request probably has no Authorization header: the handler adds nothing when the settings name is wrong, when AuthenticationType does not match the caller, or when the provider is not registered or fails. See HttpClient and gRPC clients.

An item with the same key has already been added

An ArgumentException thrown the first time the settings are read means that two registrations wrote the same settings name. Common causes:

  • AddOnBehalfOf(configuration) called in addition to the configuration based AddOidcAuthentication or AddHybridAuthentication, which already call it. The same applies to AddRemoteSecretsAuthentication with the configuration based AddJwtAuthentication.
  • An entry named like a settings name that Arc4u uses, for example a ClientTokens entry named OAuth2 or Cookies. Give each entry a unique name.

See also