.NET
Protect ASP.NET Core APIs, call APIs from workers with client credentials and sign SPAs in through a Backend-for-Frontend with the Authio .NET packages.
The Authio .NET SDK is three packages on NuGet, version 0.4.0, for .NET 8 and .NET 10. They build on the standard ASP.NET Core handlers — JWT bearer, OpenID Connect, cookies — configured from your realm’s discovery document, with Authio’s claims, roles and errors already understood.
Pick a package
| Package | Use it for |
|---|---|
| Authio.AspNetCore | An API that receives access tokens: JWT bearer validation, policies by scope, role, permission, organization, client and sign-in strength, and optional introspection. |
| Authio.AspNetCore.Bff | The host of a SPA (React, Angular, Vue…) that signs in with Authio without tokens in the browser. |
| Authio.Client | Anything else, with no ASP.NET Core dependency: workers and CLIs calling APIs with client credentials, every token grant, introspection, revocation, webhook verification and the Admin API. |
dotnet add package Authio.AspNetCore # an API
dotnet add package Authio.AspNetCore.Bff # the host of a SPA
dotnet add package Authio.Client # a worker, a CLI, or calls to AuthioPoint at your realm
All three packages read the same Authio section. The issuer is {Authority}/realms/{Realm} (or set Issuer directly), and every endpoint is read from its discovery document, cached for an hour and refused if it names a different issuer. ClientAuthentication is Basic by default; set Post or None (a public client) to match the method registered on the client.
{
"Authio": {
"Authority": "https://authio.api.hyperio.tech",
"Realm": "acme",
"ClientId": "acme-web",
"ClientSecret": "YOUR_CLIENT_SECRET"
}
}dotnet user-secrets locally and the Authio__ClientSecret environment variable in production. A missing or invalid setting stops the app at start-up with the key’s name, such as Authio:ClientId is required.Protect an API
An access token is addressed to an audience (aud), and by default that audience is Authio — every token of the realm carries it. Register your API as a resource server in Resource servers, say https://api.example.com with the scopes orders:read and orders:write, allow the clients that call it, and accept only its identifier. Clients then ask for resource=https://api.example.com and their tokens carry it as aud.
using Authio.AspNetCore.Defaults;
using Authio.AspNetCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(AuthioJwtBearerDefaults.AuthenticationScheme)
.AddAuthioJwtBearer(builder.Configuration.GetSection("Authio"));
builder.Services.AddAuthioAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/orders", (HttpContext context) => context.GetAuthioUser())
.RequireAuthioScope("orders:read");
app.MapDelete("/orders/{id}", (string id) => Results.NoContent())
.RequireAuthioPermission("orders", "delete");
app.Run();| Check | What the handler requires |
|---|---|
| Issuer and audience | Exactly the realm’s issuer, and one of Audiences. |
| Signature and lifetime | A key from the realm’s JWKS with an asymmetric algorithm (RS, PS, ES); exp and nbf with 30 seconds of ClockSkew. |
| Token type | The typ claim must be Bearer, so ID and refresh tokens are refused. |
| Claims | Kept as Authio names them (sub, scope, azp); roles flattened into role and permission claims, so IsInRole works. |
Require scopes, roles and permissions
Every requirement is available on minimal API endpoints and route groups, in policies, and as controller attributes, which need AddAuthioAuthorization().
using Authio.AspNetCore.Enums;
using Authio.AspNetCore.Extensions;
app.MapGet("/orders/export", () => Results.Ok())
.RequireAuthioScope(AuthioMatch.All, "orders:read", "orders:export");
app.MapGroup("/admin")
.RequireAuthioRole("admin")
.RequireAuthioAuthenticationMethod("mfa");| Requirement | Helper | Passes when |
|---|---|---|
| Scope | RequireAuthioScope / [AuthioScope] | Any (or, with AuthioMatch.All, every) scope is in scope. |
| Role | RequireAuthioRole / [AuthioRole] | The user holds the role, case-insensitive. Needs the roles scope. |
| Permission | RequireAuthioPermission / [AuthioPermission] | One of the user’s roles grants the method on the resource, such as orders / write. |
| Organization | RequireAuthioOrganization / [AuthioOrganization] | org_id is present, or matches the alias or ID you name. |
| Sign-in strength | RequireAuthioAuthenticationLevel / RequireAuthioAuthenticationMethod | acr reaches the level, or amr holds the method (mfa, otp, swk…). |
| Client | RequireAuthioClient / [AuthioClient] | azp is one of the clients — useful to admit one machine client. |
A missing scope answers 403 with WWW-Authenticate: Bearer error="insufficient_scope" and the scopes needed; a failed sign-in-strength requirement answers insufficient_user_authentication, so the client can send the person through a stronger sign-in. WriteProblemDetails = true in AddAuthioAuthorization adds a problem details body.
Read the caller
app.MapGet("/me", (HttpContext context) =>
{
var user = context.GetAuthioUser()!;
return new { user.Subject, user.ClientId, user.OrganizationAlias, user.Roles, user.Scopes };
}).RequireAuthorization();AuthioUser also carries the session (SessionId), the permissions per resource, Acr and AuthenticationMethods, IdentityProvider for a federated sign-in, the Actor chain of a delegated token, and the profile claims. In controllers use User.GetAuthioUser(); context.GetAuthioAccessToken() returns the raw bearer token.
Catch revoked tokens
A JWT stays valid until it expires, even after a sign-out, a revocation or a suspension. Set Introspection to ActiveCheck and the API asks Authio, after local validation, whether the token is still active — caching an active answer per token for 30 seconds, and refusing the request if Authio cannot be reached. It needs ClientId and ClientSecret of a confidential client that is the resource server’s introspection client, and builder.Services.AddAuthioClient(builder.Configuration.GetSection("Authio")) next to the bearer scheme.
{
"Authio": {
"Authority": "https://authio.api.hyperio.tech",
"Realm": "acme",
"ClientId": "orders-api",
"ClientSecret": "YOUR_CLIENT_SECRET",
"Audiences": [ "https://api.example.com" ],
"Introspection": "ActiveCheck"
}
}IntrospectionOnly validates by introspection alone, for clients that issue HMAC-signed or encrypted tokens that are not in the JWKS.
Call an API from a worker
A background service has no user: it authenticates as itself with the client_credentials grant. AddAuthioClientCredentials puts a cached token on every request of an HttpClient, renews it before it expires, shares one request among concurrent callers and, on a 401 from the API, fetches a new token and retries once.
using Authio.Client.Extensions;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddAuthioClient(builder.Configuration.GetSection("Authio"));
builder.Services
.AddHttpClient<OrdersApi>(client => client.BaseAddress = new Uri("https://api.example.com"))
.AddAuthioClientCredentials(options =>
{
options.Scopes = ["orders:read"];
options.Resource = "https://api.example.com";
});
builder.Services.AddHostedService<NightlyReport>();
builder.Build().Run();The client needs the client_credentials grant and must be allowed on the resource server. To get a token yourself, inject IAuthioClientCredentialsTokenCache and call GetTokenAsync(["orders:read"], "https://api.example.com"). Credentials, consumers and rotation are covered in Machine to machine.
Sign a SPA in through a BFF
Tokens kept in the browser can be read by any script on the page. With Authio.AspNetCore.Bff the SPA and its ASP.NET Core host share an origin: the host is a confidential client that runs the code flow with PKCE, keeps the tokens server-side and refreshes them, and the browser only holds an HttpOnly __Host-authio cookie. Every API call from the SPA carries an X-CSRF: 1 header, which a cross-site page cannot add.
Register the client
A confidential client with the
authorization_codegrant, the redirect URIhttps://app.example.com/signin-oidc, the post-logout redirect URIhttps://app.example.com/signout-callback-oidc, and the scopesopenid profile email offline_access—offline_accessgives the refresh token — plusrolesif you setRequestRoles, and your API scopes.Configure the host
Program.csC#using Authio.AspNetCore.Bff.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthioBff(builder.Configuration.GetSection("Authio")); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthioBff(); app.UseAuthorization(); app.MapAuthioBffEndpoints(); app.MapGet("/api/me", (HttpContext context) => new { name = context.User.FindFirst("name")?.Value }) .RequireAuthorization() .AsAuthioBffApiEndpoint(); app.MapAuthioRemoteApi("/api/orders", "https://api.example.com/orders"); app.Run();Call it from the SPA
api.tsTypeScriptexport const api = (url: string, init: RequestInit = {}) => fetch(url, { ...init, credentials: 'same-origin', headers: { ...init.headers, 'X-CSRF': '1' }, }) const response = await api('/bff/user') if (response.status === 401) { window.location.href = '/bff/login?returnUrl=' + encodeURIComponent(location.pathname) }
| Endpoint | What it does |
|---|---|
/bff/login | Starts the sign-in. Takes returnUrl (local only) and optionally prompt, login_hint, ui_locales, organization, max_age and resource. |
/bff/user | With X-CSRF: 1: the session’s claims, one role and permission per grant, bff:session_expires_in and bff:logout_url. 401 when signed out, never a redirect. |
/bff/logout | Navigate to the bff:logout_url value: it revokes the refresh token, deletes the session and signs out at Authio. |
MapAuthioRemoteApi forwards to the API with the user’s access token attached and cookies stripped. Every 5 minutes the BFF introspects the session’s refresh token and signs the person out if their Authio session ended. For more than one instance, register a shared IDistributedCache (Redis) and persist the Data Protection keys before AddAuthioBff.