Skip to content

.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

PackageUse it for
Authio.AspNetCoreAn API that receives access tokens: JWT bearer validation, policies by scope, role, permission, organization, client and sign-in strength, and optional introspection.
Authio.AspNetCore.BffThe host of a SPA (React, Angular, Vue…) that signs in with Authio without tokens in the browser.
Authio.ClientAnything 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.
TerminalBash
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 Authio

Point 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.

appsettings.jsonJSON
{
  "Authio": {
    "Authority": "https://authio.api.hyperio.tech",
    "Realm": "acme",
    "ClientId": "acme-web",
    "ClientSecret": "YOUR_CLIENT_SECRET"
  }
}
Keep the secret out of source control: use 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.

Program.csC#
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();
CheckWhat the handler requires
Issuer and audienceExactly the realm’s issuer, and one of Audiences.
Signature and lifetimeA key from the realm’s JWKS with an asymmetric algorithm (RS, PS, ES); exp and nbf with 30 seconds of ClockSkew.
Token typeThe typ claim must be Bearer, so ID and refresh tokens are refused.
ClaimsKept 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().

Minimal APIsC#
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");
RequirementHelperPasses when
ScopeRequireAuthioScope / [AuthioScope]Any (or, with AuthioMatch.All, every) scope is in scope.
RoleRequireAuthioRole / [AuthioRole]The user holds the role, case-insensitive. Needs the roles scope.
PermissionRequireAuthioPermission / [AuthioPermission]One of the user’s roles grants the method on the resource, such as orders / write.
OrganizationRequireAuthioOrganization / [AuthioOrganization]org_id is present, or matches the alias or ID you name.
Sign-in strengthRequireAuthioAuthenticationLevel / RequireAuthioAuthenticationMethodacr reaches the level, or amr holds the method (mfa, otp, swk…).
ClientRequireAuthioClient / [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

Program.csC#
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.

appsettings.jsonJSON
{
  "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.

Program.csC#
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.

  1. Register the client

    A confidential client with the authorization_code grant, the redirect URI https://app.example.com/signin-oidc, the post-logout redirect URI https://app.example.com/signout-callback-oidc, and the scopes openid profile email offline_access — offline_access gives the refresh token — plus roles if you set RequestRoles, and your API scopes.

  2. 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();
  3. Call it from the SPA

    api.tsTypeScript
    export 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)
    }
EndpointWhat it does
/bff/loginStarts the sign-in. Takes returnUrl (local only) and optionally prompt, login_hint, ui_locales, organization, max_age and resource.
/bff/userWith 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/logoutNavigate 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.

Next steps