Table of Contents

Server authentication

Arc4u configures the ASP.NET Core authentication handlers (JWT bearer, OpenID Connect with a cookie, or both) from the Authentication section of your configuration, and turns every authenticated user into an AppPrincipal: a ClaimsPrincipal that also carries the user profile and the Arc4u authorization (roles, operations and scopes). Read this guide when you build an API or a web application that must authenticate users against an identity provider such as Microsoft Entra ID, Azure AD B2C, ADFS, Keycloak or ForgeRock. The principal is stored in the application context, described in Concepts. The services are registered through dependency injection, as described in Design principles.

What it solves

ASP.NET Core already validates tokens and runs the OpenID Connect protocol. Arc4u adds:

  • one registration call per scenario that reads a documented configuration section and wires the handlers, the events, the cookie and the data protection keys consistently;
  • the refresh of the user's access token in a cookie session, before it expires;
  • the creation of an AppPrincipal for each request: profile, authorization and optional extra claims loaded once and cached (claims filler);
  • authorization policies built from the Arc4u operations, usable with [Authorize(Policy = ...)];
  • a few middlewares for real deployments: forced login on browser paths, TLS termination in front of the application, Basic credentials converted into a bearer token.

Arc4u leaves the protocol itself to ASP.NET Core (Microsoft.AspNetCore.Authentication.JwtBearer and Microsoft.AspNetCore.Authentication.OpenIdConnect) and the storage of tokens, tickets and keys to the Arc4u caches.

The following diagram shows what happens to an authenticated request.

flowchart LR
    Request[HTTP request] --> Handler["ASP.NET Core handler (JWT bearer or cookie + OpenID Connect)"]
    Handler --> Transform["AppPrincipalTransform (IClaimsTransformation)"]
    Transform --> Filler["IClaimsFiller (extra claims, cached)"]
    Transform --> Context["IApplicationContext.Principal (AppPrincipal)"]
    Context --> Policies["Authorization policies and your code"]

Packages involved

Package Use it for
Arc4u.OAuth2.AspNetCore.Authentication AddJwtAuthentication, AddOidcAuthentication and AddHybridAuthentication, their events, the cookie ticket store and the data protection key store.
Arc4u.OAuth2.AspNetCore The creation of the AppPrincipal (AppPrincipalTransform), the middlewares (forced OpenID Connect login, Basic authentication, bearer injection, resource rights) and the server-side token providers.
Arc4u.OAuth2 The options shared by all scenarios (authority, token cache, claims filler, claim identifiers) and the default claims filler.
Arc4u.Authorization Authorization policies built from Arc4u operations and scopes.

Arc4u.OAuth2.AspNetCore.Authentication references Arc4u.OAuth2.AspNetCore, which references Arc4u.OAuth2. Install Arc4u.Authorization only if you use its policies.

Install

dotnet add package Arc4u.OAuth2.AspNetCore.Authentication --prerelease
dotnet add package Arc4u.Authorization --prerelease

The samples of this guide also use a memory cache and the JSON serializer:

dotnet add package Arc4u.Caching.Memory --prerelease
dotnet add package Arc4u.Serializer.JSon --prerelease

You need an application registered in your identity provider: its authority URL, and for OpenID Connect a client id, a client secret and the redirect URI https://<your-host>/signin-oidc. Identity providers lists what is specific to each provider.

Configuration

Each registration method reads the Authentication section; pass authenticationSectionName to use another one. The section paths below are the defaults: each one can be changed by a ...SectionPath key of the scenario (see the scenario pages). The keys specific to each scenario are described in JWT bearer, OpenID Connect and cookie and Hybrid.

appsettings.json

The smallest configuration, for an API that accepts JWT bearer tokens:

{
  "Caching": {
    "Default": "Volatile",
    "Caches": [
      {
        "Name": "Volatile",
        "Kind": "Memory",
        "IsAutoStart": true,
        "Settings": { "SizeLimitInMB": 10 }
      }
    ]
  },
  "Authentication": {
    "DefaultAuthority": { "Url": "https://login.example.com/my-tenant/v2.0" },
    "OAuth2.Settings": { "Audiences": [ "api://my-api" ] },
    "TokenCache": { "CacheName": "Volatile" }
  }
}

The Caching section is described in the caching guide. The sections shared by all scenarios are:

Key Type Default Description
Authentication:DefaultAuthority:Url URI none (required) Base URL of the identity provider.
Authentication:DefaultAuthority:MetaDataAddress URI Url + /.well-known/openid-configuration Address of the OpenID Connect metadata document. Set it when the provider publishes the document elsewhere. When it starts with http://, HTTPS is not required for the metadata.
Authentication:DefaultAuthority:Issuer URI read from the metadata Expected iss claim of the access tokens, when ValidateAuthority is true. Set it when your tokens have another issuer than the metadata.
Authentication:DefaultAuthority:TokenEndpoint URI read from the metadata Token endpoint, used by the token providers.
Authentication:TokenCache:CacheName string none (required when the section exists) Name of the named cache that stores tokens and extra claims. When the name is unknown, the default cache is used. With JWT bearer, the TokenCache section is optional.
Authentication:TokenCache:MaxTime TimeSpan 00:50:00 Lifetime of a cached entry, including the extra claims of a user.
Authentication:ClaimsIdentifier string array http://schemas.microsoft.com/identity/claims/objectidentifier, oid Claim types that identify a user. The first one found is used as cache key for the extra claims.
Authentication:ClaimsMiddleWare:ClaimsFiller object see Claims and authorization Extra claims loaded when the principal is created.
Authentication:DomainsMapping object empty Maps the domain part of a upn claim to the domain of the user profile, for example "contoso.com": "CONTOSO".

Code

AddJwtAuthentication, AddOidcAuthentication and AddHybridAuthentication register the handlers and their options. The services that create the AppPrincipal and the caches are registered by your application:

// Program.cs
using Arc4u.Caching;
using Arc4u.Caching.Memory;
using Arc4u.OAuth2;
using Arc4u.OAuth2.Extensions;
using Arc4u.OAuth2.Security;
using Arc4u.OAuth2.Security.Principal;
using Arc4u.OAuth2.Token;
using Arc4u.Security.Principal;
using Arc4u.Serializer;
using Microsoft.AspNetCore.Authentication;

var builder = WebApplication.CreateBuilder(args);

// Application context (scoped), logger and activity source.
builder.Services.AddApplicationContext();

// Caches for the tokens and the extra claims.
builder.Services.AddCacheContext(builder.Configuration);
builder.Services.AddMemoryCacheKind();
builder.Services.AddSingleton<IObjectSerialization, JsonSerialization>();

// Creation of the AppPrincipal.
builder.Services.AddScoped<IClaimsTransformation, AppPrincipalTransform>();
builder.Services.AddSingleton<IClaimProfileFiller, ClaimsProfileFiller>();
builder.Services.AddSingleton<IClaimAuthorizationFiller, ClaimsAuthorizationFiller>();
builder.Services.AddTransient<IClaimsFiller, ClaimsBearerTokenExtractor>();
builder.Services.AddSingleton<ICacheHelper, CacheHelper>();
builder.Services.AddSingleton<ICacheKeyGenerator, KeyGeneratorFromIdentity>();
builder.Services.AddSingleton<IUserObjectIdentifier, UserObjectIdentifier>();

// Authentication and authorization.
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/me", (IApplicationContext context) => context.Principal!.Profile.DisplayName)
   .RequireAuthorization();

app.Run();
  • AddAuthorization() is required: the Arc4u methods only call AddAuthorizationCore(), and UseAuthorization() throws without it.
  • AddApplicationContext() registers IApplicationContext as a scoped service: each request has its own principal.
Tip

The Arc4u types above carry [Export] attributes, so the Arc4u.Dependency.Tool source generator can register them from the Application.Dependency:RegisterTypes list of your configuration (see Dependency injection). Keep AddApplicationContext() in code: ApplicationInstanceContext is exported as a singleton, which is only right for desktop applications.

Common scenarios

Protect an API with JWT bearer tokens

Call AddJwtAuthentication. Clients send Authorization: Bearer <token>; the audience, the signature and the lifetime of the token are validated. See JWT bearer, which also shows how to accept Basic credentials.

Sign users in to a web application

Call AddOidcAuthentication. Unauthenticated requests are redirected to the identity provider, the user session is kept in a cookie and the access token is refreshed before it expires. See OpenID Connect and cookie.

Serve pages and an API from the same application

Call AddHybridAuthentication. Requests with a Bearer header use JWT bearer, the others use the cookie. See Hybrid.

Grant access from the user's operations

Load the user's rights with a claims filler and protect endpoints with policies named after the Arc4u operations. See Claims and authorization.

Run behind a reverse proxy or with a private certificate authority

When TLS ends before the application, see TLS termination. To trust services whose certificates are issued by your own authority, see Custom root CA.

Call another API with the user's token

Obtaining tokens for outgoing calls (on-behalf-of, client credentials, HttpClient and gRPC handlers) is covered in Client authentication.

Extensibility points

The registration methods add their defaults with TryAdd, or you register the services yourself (see Code). Register your implementation instead of the default one.

Service Default implementation Replace it to
IClaimsFiller ClaimsBearerTokenExtractor Load the user's rights and extra claims from your own store.
IClaimAuthorizationFiller ClaimsAuthorizationFiller Build the Authorization of the principal another way than from the Arc4u authorization claim.
IClaimProfileFiller ClaimsProfileFiller Build the UserProfile from other claims.
IClaimsTransformation AppPrincipalTransform Create the principal yourself.
IUserObjectIdentifier UserObjectIdentifier Identify a user from something else than the ClaimsIdentifier claim types.
JwtBearerEvents StandardBearerEvents Change the 401 response or add checks on the token. Register it before the Add...Authentication call.
OpenIdConnectEvents StandardOpenIdConnectEvents Change the redirection to the identity provider or the error pages. Register it before the Add...Authentication call.
CookieAuthenticationEvents StandardCookieEvents Change the validation and refresh of the cookie session. Register it before the Add...Authentication call.
ITicketStore CacheTicketStore Store the cookie session elsewhere. Register it before the Add...Authentication call.
ITokenRefreshProvider RefreshTokenProvider Refresh the tokens of a cookie session differently.
ITokenProvider (keyed) Oidc, Bootstrap, Obo, Credential, ... Provide a token for a ProviderId of your settings.

For example, to write your own ProblemDetails in the 401 response (here, a default title when the handler gives no error), derive from StandardBearerEvents and override Challenge (the override keeps the x-token-expired header of the base class):

// MyBearerEvents.cs
using Arc4u.OAuth2.Events;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

public sealed class MyBearerEvents(ILogger<StandardBearerEvents> logger) : StandardBearerEvents(logger)
{
    public override async Task Challenge(JwtBearerChallengeContext context)
    {
        context.HandleResponse();
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        if (context.AuthenticateFailure is SecurityTokenExpiredException expired)
        {
            context.Response.Headers.Append("x-token-expired", expired.Expires.ToString("o"));
        }

        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = context.Error ?? "Unauthorized",
            Detail = context.ErrorDescription,
            Status = StatusCodes.Status401Unauthorized,
        });
    }
}
using Arc4u.OAuth2.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.AddTransient<JwtBearerEvents, MyBearerEvents>();
builder.Services.AddJwtAuthentication(builder.Configuration);

Troubleshooting

The application throws at startup

Most registration errors name the missing key: DefaultAuthority must be filled!, Audiences field is not defined., TokenCacheOptions.CacheName is not defined in the configuration file. or Unable to find the required services... 'IServiceCollection.AddAuthorization'. See Troubleshooting.

Every request returns 401 or 500

See Troubleshooting, which lists the symptoms reported in past issues with their causes, and the known issues of develop/9.0.0.

See also