SoftwareDriven.Authentication.ApiKey.Server 5.0.0

dotnet add package SoftwareDriven.Authentication.ApiKey.Server --version 5.0.0
                    
NuGet\Install-Package SoftwareDriven.Authentication.ApiKey.Server -Version 5.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SoftwareDriven.Authentication.ApiKey.Server" Version="5.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SoftwareDriven.Authentication.ApiKey.Server" Version="5.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SoftwareDriven.Authentication.ApiKey.Server" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SoftwareDriven.Authentication.ApiKey.Server --version 5.0.0
                    
#r "nuget: SoftwareDriven.Authentication.ApiKey.Server, 5.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SoftwareDriven.Authentication.ApiKey.Server@5.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SoftwareDriven.Authentication.ApiKey.Server&version=5.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SoftwareDriven.Authentication.ApiKey.Server&version=5.0.0
                    
Install as a Cake Tool

SoftwareDriven.Authentication

Authentication building blocks for ASP.NET Core / Blazor apps talking to an OpenID Connect provider (Keycloak) — plus API key authentication for service-to-service calls.

Package Purpose
SoftwareDriven.Authentication.OpenIdConnect.Shared Common models (OidcConfig, AccessTokenResponse) and configuration helpers.
SoftwareDriven.Authentication.OpenIdConnect.Server JWT bearer authentication for APIs (validate access tokens issued by the OIDC provider), Keycloak role flattening, guest scheme.
SoftwareDriven.Authentication.ApiKey.Server X-Api-Key authentication scheme with per-key roles and an authorization requirement.
SoftwareDriven.Authentication.OpenIdConnect.WebApp.Server Blazor Web App server side: cookie session + named OIDC schemes, login/logout/after-login endpoints, transparent token refresh, access-token handoff endpoint, bearer handler for prerendering.
SoftwareDriven.Authentication.OpenIdConnect.WebApp.Client Blazor WebAssembly side of the above: IAccessTokenService, BearerTokenHandler (401 retry), RedirectToLogin component.

All packages share one version. Target framework: net10.0.

5.0: the former SoftwareDriven.Authentication.OpenIdConnect.Client package (WASM-side ROPC / RemoteAuthenticatorView stack with tokens in local storage) was removed and replaced by the WebApp.* pair. See Migration.


OpenIdConnect.Server — JWT bearer for APIs

builder.Services.AddOpenIdConnect(builder.Configuration, jwt =>
{
    // optional: adjust JwtBearerOptions after the defaults were applied
    jwt.RequireHttpsMetadata = builder.Environment.IsProduction();
});
builder.Services.Configure<KeycloakClaimsTransformationOptions>(o => o.RoleClaimType = ClaimTypes.Role); // default

Configuration (Oidc:*): IssuerUrl (authority), Audience, optional MetadataUrl, ClientId, NameClaim (default preferred_username).

  • Registers JWT bearer as default scheme and ClaimsTransformerKeycloak, which flattens realm_access.roles / roles into role claims on every request (deduplicated).
  • AddGuestAuthentication() registers an always-succeeding Guest scheme (local development).
  • Diagnostics: IdentityModelEventSource.ShowPII is no longer switched on by the package; set it yourself in Development if needed.

ApiKey.Server

// as default scheme
builder.Services.AddApiKeyAuthentication(o => o.AddApiKey("secret-key", ["Reader"]));

// or next to another default scheme (does not touch AddAuthentication defaults)
builder.Services.AddAuthentication(...).AddApiKeySupport(o => o.AddApiKey("secret-key", ["Reader"]));

The key is read from the X-Api-Key header (or ?apiKey= query parameter). Roles configured via AddApiKey become role claims. ApiKeyRequirement + ApiKeyAuthorizationHandler allow policies restricted to specific keys.


WebApp.Server + WebApp.Client — Blazor Web App (InteractiveWebAssembly)

Architecture

  • Login without WebAssembly. The OIDC code flow (authorization code + PKCE) runs entirely on the server; the user gets a cookie. Login pages can be static SSR — the login buttons are plain links to /auth/login?scheme=….
  • Named schemes instead of subdomains. Every user group / identity provider (e.g. Teacher, Student, Vidis) is one OIDC scheme in the same app, configured under Oidc:{Scheme}. Which scheme a user signed in with is stored as a claim in the cookie ticket (WebAppOidcOptions.SchemeClaimType, default sd_auth_scheme) — needed for logout and refresh.
  • Token handoff instead of BFF proxy. The WASM client fetches the access token from the same-origin endpoint GET /auth/token (~ once per token lifetime), keeps it in memory and calls the resource APIs directly with a bearer header. The refresh token never leaves the server; nothing is stored in local storage.
  • Transparent refresh in the cookie validation. When the access token in the ticket is about to expire, the cookie handler's OnValidatePrincipal refreshes it at the identity provider and re-issues the ticket. This keeps prerender / SSR API calls (which take the token from the ticket via ServerBearerTokenHandler) working after inactivity, and it ends the cookie session when the identity provider refuses the refresh (no "zombie login"). The token endpoint therefore is a fast path that almost always just returns the token from the ticket.
  • Auth state to WASM. Roles are flattened once at sign-in (KeycloakClaimsEnricher) and reach the client via AddAuthenticationStateSerialization(o => o.SerializeAllClaims = true) / AddAuthenticationStateDeserialization().

appsettings.json (server)

{
  "Oidc": {
    "Teacher": { "Authority": "https://idm.example.org/realms/school", "ClientId": "school.teacher" },
    "Student": { "Authority": "https://idm.example.org/realms/school", "ClientId": "school.student" },
    "Vidis":   { "Authority": "https://idm.example.org/realms/school", "ClientId": "school.vidis" }
  }
}

Per scheme: Authority, ClientId, ClientSecret (confidential clients only — user secrets / Helm values, never in git), Scope (default openid profile roles offline_access), DisplayName, NameClaim (default preferred_username), RequireHttpsMetadata (default true), CallbackPath (default /signin-oidc-{scheme}), SignedOutCallbackPath (default /signout-callback-oidc-{scheme}). Sections without Authority are skipped, so an environment can offer a subset of schemes. Register https://{host}/signin-oidc-{scheme} and https://{host}/signout-callback-oidc-{scheme} (lower-case scheme) as redirect URIs at the identity provider.

Server Program.cs

using SoftwareDriven.Authentication.OpenIdConnect.WebApp.Server;
using SoftwareDriven.Authentication.OpenIdConnect.WebApp.Server.Helper;

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents()
    .AddAuthenticationStateSerialization(o => o.SerializeAllClaims = true);

builder.Services.AddOidcWebAppAuthentication(builder.Configuration, sectionName: "Oidc",
    configure: o =>
    {
        o.AfterLoginRedirect = WebAppOidcOptions.RedirectByRole(new Dictionary<string, string>
        {
            ["Teacher"] = "/teacher",
            ["Student"] = "/student",
        });
    },
    configureCookie: c =>
    {
        c.LoginPath = "/";
        c.ExpireTimeSpan = TimeSpan.FromHours(8);
        c.SlidingExpiration = true;
    });

// API clients used during prerendering: bearer token from the cookie ticket
builder.Services.AddHttpClient("api", c => c.BaseAddress = new Uri(apiUrl))
    .AddHttpMessageHandler<ServerBearerTokenHandler>();

// Replica > 1: shared data protection key ring, otherwise cookies from other instances are unreadable
builder.Services.AddDataProtection().SetApplicationName("my-portal").PersistKeysToFileSystem(new DirectoryInfo("/keys"));

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapOidcWebAppEndpoints();          // before MapRazorComponents
app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

Building blocks for consumers that own AddAuthentication(): AddOidcWebAppCookie(...), AddOidcSchemes(config, "Oidc"), AddOidcScheme(new OidcSchemeConfig { ... }), AddOidcWebAppCoreServices().

Client Program.cs

using SoftwareDriven.Authentication.OpenIdConnect.WebApp.Client;

builder.Services.AddOidcWebAppClient(o => o.LoginPath = "/");   // includes AddAuthenticationStateDeserialization()
builder.Services.AddHttpClient("api", c => c.BaseAddress = new Uri(apiUrl))
    .AddBearerTokenHandler();

Routes.razor:

@using SoftwareDriven.Authentication.OpenIdConnect.WebApp.Client.Components
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
    <NotAuthorized><RedirectToLogin /></NotAuthorized>
</AuthorizeRouteView>

RedirectToLogin sends unauthenticated users to LoginPath?returnUrl=… (full page load); users that are signed in but lack the required role see a "no access" message instead of a redirect loop (NoAccessContent, NoAccessTitle, … parameters). Note that pages with [Authorize(Roles = …)] reached by a full page load are checked by the endpoint authorization on the server first — a signed-in user without the role is forbidden by the cookie handler (AccessDeniedPath, default /Account/AccessDenied); set c.AccessDeniedPath = "/no-access" in configureCookie and provide that page.

Login / logout links must bypass enhanced navigation so they work before and after hydration:

<a href="/auth/login?scheme=teacher" data-enhance-nav="false">Teacher login</a>
<a href="/auth/logout" data-enhance-nav="false">Logout</a>

Identity provider hints: /auth/login?scheme=vidis&kc_idp_hint=… — parameters listed in IdpHintParameters are forwarded to the authorization request.

Forcing a fresh access token / fresh claims

Identity provider mappers may put account attributes into the access token. When such attributes change at runtime (and the app must guarantee the APIs see them immediately), the client can force a refresh — the replacement for the former AccessTokenRevoker:

@inject IWebAppAuthSession Session
// ...
await Session.RefreshAsync();   // 1) GET auth/token?refresh=true → server exchanges the refresh token at the IdP,
                                //    re-syncs roles + AccessTokenClaimTypes into the cookie principal, re-issues the cookie
                                // 2) GET auth/me → WebAppAuthenticationStateProvider raises AuthenticationStateChanged
  • Server: list the claim types to mirror from the access token into the cookie principal: o.AccessTokenClaimTypes.Add("school_id") (applied at sign-in and after every refresh; ResyncClaimsOnRefresh also replaces role claims by the token's roles). AllowForcedRefresh = false disables ?refresh=true.
  • Client: WebAppAuthenticationStateProvider (registered by AddOidcWebAppClient) starts from the prerendered snapshot and can be re-read from auth/me at any time (IWebAppAuthSession.RefreshAuthenticationStateAsync()); AuthorizeView / AuthorizeRouteView re-render through the normal AuthenticationStateChanged event.
  • Only the token is needed (no UI update)? IAccessTokenService.RefreshAsync().
  • Because the regular cookie refresh runs the same re-sync, claims in the cookie ticket now follow the access token at every refresh (roles + AccessTokenClaimTypes); other claims (ID token / user info) still only change at a real login.

Options reference

WebAppOidcOptions (server):

Option Default Notes
CookieScheme Cookies Scheme the OIDC schemes sign in to.
SchemeClaimType sd_auth_scheme Claim remembering the OIDC scheme used.
IdpHintParameters kc_idp_hint, vidis_idp_hint Login query parameters passed through to the IdP.
EndpointPrefix /auth Route prefix; LoginPath, LogoutPath, TokenPath, AfterLoginPath derive from it. Must match the client's TokenEndpoint ({prefix}/token).
DefaultRedirectPath / Login without / unknown scheme, after-login fallback.
SchemeNotConfiguredRedirectPath /?error=scheme-not-configured
AfterLoginRedirect _ => "/" Func<ClaimsPrincipal,string>; use WebAppOidcOptions.RedirectByRole(...).
PostLogoutRedirectPath /
EnrichKeycloakRoles / RoleClaimType true / ClaimTypes.Role Role flattening at sign-in.
MinimumRemainingTokenLifetime 60 s Refresh threshold (cookie validation and token endpoint).
AccessTokenClaimTypes Claims mirrored from the access token into the cookie principal (sign-in + every refresh).
ResyncClaimsOnRefresh true Replace roles + AccessTokenClaimTypes from the new token after a refresh.
AllowForcedRefresh true Honour {TokenPath}?refresh=true.
RefreshOnValidatePrincipal true Transparent refresh + reject-on-failure in the cookie validation.
UsePkce true
ResponseMode query Together with Lax correlation/nonce cookies works on plain http://localhost dev hosts.
CorrelationAndNonceCookieSameSite Lax
ConfigureScheme Action<string, OpenIdConnectOptions> last-chance hook per scheme.

WebAppAuthClientOptions (client):

Option Default Notes
TokenEndpoint auth/token Relative to the host base address. Must match the server's EndpointPrefix.
MeEndpoint auth/me Auth state endpoint; must match the server prefix as well.
LoginPath / Target on 401 from the token endpoint and for RedirectToLogin. Must match the server's cookie LoginPath / login page.
ReturnUrlParameterName returnUrl
MinimumRemainingTokenLifetime 30 s Client-side cache threshold.
RedirectOnUnauthorized true

Session lifetime, refresh and known limits

  • The cookie's ExpireTimeSpan is only an upper bound. The effective session length is decided by the identity provider (SSO Session Idle / Max): once the IdP refuses the refresh, the cookie is rejected and the next protected request challenges again — a silent re-login while the SSO session is alive, otherwise a real login. Recommendation: align the cookie lifetime with the IdP's "SSO Session Max".
  • Claims staleness. The refresh re-syncs roles and AccessTokenClaimTypes from the new access token, but not the other claims in the cookie ticket (dotnet/aspnetcore#58826); role / profile changes take effect at the next real login.
  • Distributed lock. Refreshes are serialized per user with IRefreshLock; the default InProcessRefreshLock guards one process. If the realm rotates refresh tokens ("Revoke Refresh Token") and the app runs with more than one replica, register a distributed implementation (services.AddSingleton<IRefreshLock, MyRedisRefreshLock>() before AddOidcWebAppAuthentication).
  • Cookie size / reverse proxy. Tokens live in the (chunked) cookie ticket. Behind ingress-nginx set proxy-buffer-size: 32k and proxy-buffers-number: 4, otherwise the OIDC callback answers 502. Alternatively move the ticket into an ITicketStore (CookieAuthenticationOptions.SessionStore).
  • Replicas need a shared data protection key ring (SetApplicationName + PersistKeysToFileSystem/Redis), otherwise users get logged out at random.
  • If the consumer sets CookieAuthenticationOptions.EventsType, the package's event hooks (OnRedirectToLogin → 401 for the token path, OnValidatePrincipal refresh) are bypassed — wire them into your events class instead.

Migration 4.x → 5.0

  • SoftwareDriven.Authentication.OpenIdConnect.Client is gone (AuthenticationService, ITokenStorage, LocalStorageTokenStore, TokenAuthenticationStateProvider, RefreshTokenService, AddOpenIdConnect(...) for WASM, AccessTokenRevoker). Blazor WASM apps that keep the old RemoteAuthenticatorView flow stay on 4.x; new / migrated apps use the Blazor Web App stack (WebApp.Server + WebApp.Client).
  • Shared: ConfigurationExtensions.GetOidcClientOptions() removed (with the IdentityModel.OidcClient dependency); AccessTokenResponse added.
  • Server: ClaimsTransformerKeycloak.ConfigureRoleClaim(...) (static) removed → services.Configure<KeycloakClaimsTransformationOptions>(...). AddOpenIdConnect(...) got an optional Action<JwtBearerOptions> hook. IdentityModelEventSource.ShowPII is no longer forced on.
  • ApiKey.Server: ApiKeyAuthenticationOptions.AuthenticationType is a property now; handler constructor without ISystemClock.
  • All packages: net10.0, version 5.0.0, framework reference Microsoft.AspNetCore.App instead of individual Microsoft.AspNetCore.* packages.

Build / pack / publish

dotnet build SoftwareDriven.Authentication.sln -c Release
dotnet pack  SoftwareDriven.Authentication.sln -c Release -o .\artifacts
dotnet nuget push .\artifacts\SoftwareDriven.Authentication.*.nupkg -s https://api.nuget.org/v3/index.json -k <key>

Version and shared package metadata live in Directory.Build.props.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.0 32 8/15/2026
3.0.0 107 7/15/2026
2.0.1 300 11/5/2025
2.0.0 246 11/5/2025
1.2.0 330 5/8/2025
1.1.1 223 1/31/2025
1.1.0 200 1/30/2025
1.0.0 191 1/30/2025