Authio.AspNetCore 0.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Authio.AspNetCore --version 0.8.0
                    
NuGet\Install-Package Authio.AspNetCore -Version 0.8.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="Authio.AspNetCore" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Authio.AspNetCore" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Authio.AspNetCore" />
                    
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 Authio.AspNetCore --version 0.8.0
                    
#r "nuget: Authio.AspNetCore, 0.8.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 Authio.AspNetCore@0.8.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=Authio.AspNetCore&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Authio.AspNetCore&version=0.8.0
                    
Install as a Cake Tool

Authio.AspNetCore

Protect an ASP.NET Core API (.NET 8 and .NET 10) with Authio:

  • Access-token validation with the standard JWT bearer handler, configured from the realm's discovery document: issuer, explicit audiences, the client's access token algorithm (asymmetric by default; HS256/HS384/HS512 with the client secret when named), small clock skew, Authio's claim names kept as they are; access tokens Authio encrypts to the client's own key (nested JWE, RSA-OAEP or RSA-OAEP-256) decrypted with Authio:ClientEncryptionKey. No client id needed to validate locally.
  • Introspection, optional: confirm on every request (cached per jti) that the token was not revoked and the session did not end, or validate opaque tokens, and HMAC tokens of another client, purely by introspection.
  • RFC 9728, opt-in: the API's protected resource metadata at /.well-known/oauth-protected-resource{path} and resource_metadata in every 401 challenge.
  • Authio roles: the *_resource_access claims are flattened into role and permission claims, and the full model is one call away.
  • Authorization: scope, role, permission (Authio's exact rule), organization, client, caller kind and step-up (acr, amr, recency) requirements, as policy-builder helpers, minimal API helpers and controller attributes, with RFC 6750 errors, the RFC 9470 step-up challenge and optional problem details.
  • A typed caller: User.GetAuthioUser(): who called (a person, a client as itself, a client's consumer or an account credential, and with which credential), subject, client, session, scopes, roles, permissions, organization, account, acr/amr, impersonation and delegation chain, profile.
  • Webhooks: a signed-delivery receiver with secret rotation, timestamp tolerance, de-duplication and typed handlers.
  • Downstream calls: forward the caller's token, or exchange it on behalf of the user (token exchange), cached.

It builds on Authio.Client; machine-to-machine calls use its AddAuthioClientCredentials.

dotnet add package Authio.AspNetCore

Quick start

{
  "Authio": {
    "Authority": "https://auth.example.com",
    "Realm": "acme",
    "Audiences": [ "https://orders.example.com" ]
  }
}
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.Run();

Token validation

AddAuthioJwtBearer registers the scheme AuthioBearer (AuthioJwtBearerDefaults.AuthenticationScheme, or any name with the (scheme, configuration, configure) overload). It reads the realm from AuthioOptions (Authority + Realm, or Issuer) and its own settings from AuthioJwtBearerOptions; the configuration overload binds both from the same section. With the Action<AuthioJwtBearerOptions> overload, configure AuthioOptions through services.AddAuthioClient(...).

What the handler checks:

Check Value
Metadata {issuer}/.well-known/openid-configuration through the Authio named HttpClient; keys from its jwks_uri
Issuer exactly {Authority}/realms/{Realm} (a different issuer in the discovery document is not trusted)
Audience any of Audiences (required)
Algorithms AccessTokenSignedResponseAlg; left out, RS256/384/512, PS256/384/512, ES256/384/512, never HS* (see Access token algorithm)
Lifetime exp/nbf required, ClockSkew default 30 s
Token type the typ claim must be Bearer: ID and refresh tokens are refused (RejectNonAccessTokens)
Claims MapInboundClaims = false: sub, scope, azp… keep their names
Name preferred_username, or sub when the token has none (client-credentials tokens)
Roles role claims flattened from *_resource_access (MapAuthorizationClaims) so IsInRole works

Access token algorithm

Authio:AccessTokenSignedResponseAlg mirrors the client's access token signing setting in Authio, its second one (the ID token has its own; the refresh token is always signed with the server's key). It takes one algorithm or several:

"AccessTokenSignedResponseAlg": "RS256"
"AccessTokenSignedResponseAlg": [ "RS256", "HS256" ]

Left out, the asymmetric algorithms are accepted and HS* never is. A token signed with an algorithm outside the list is refused before any key is tried; an HS* one, when none is listed, with a description that says what to do:

WWW-Authenticate: Bearer error="invalid_token", error_description="The token is signed with the client secret (HS*): set Authio:AccessTokenSignedResponseAlg and Authio:ClientSecret to validate it locally or validate it with introspection (Authio:Introspection = IntrospectionOnly)."

When to use HS*. HS256, HS384 and HS512 are verified with the client secret (its UTF-8 octets, at least 32, 48 or 64 bytes, RFC 7518 §3.2), so only an API that holds that secret validates such a token locally: the client's own API, deployed with it. List the algorithm and give the API Authio:ClientId and Authio:ClientSecret (or ClientSecretProvider, read on every token so a rotation is picked up). Any other API must not receive the secret: it validates the client's HS* tokens with Introspection = IntrospectionOnly. When in doubt, keep the client on an asymmetric algorithm, which every API verifies with the realm's public keys and no secret.

A secret shorter than the algorithm needs, an HS* algorithm without a secret, or an algorithm Authio does not sign with stops the API at start-up; a secret from ClientSecretProvider is measured when a token arrives, and a short one fails the request with InvalidOperationException (a configuration error, not the caller's).

An API that only validates locally needs no Authio:ClientId; introspection, on-behalf-of and the client-credentials handler authenticate the client and do.

Several realms

Each scheme validates against its own realm. Its AuthioOptions are named after the scheme: they start as a copy of the shared ones (AddAuthioClient, or the default scheme's section), then take the scheme's own section, then services.Configure<AuthioOptions>(scheme, ...) registered after AddAuthioJwtBearer. A scheme that names no realm validates against the client's.

builder.Services
    .AddAuthentication(AuthioJwtBearerDefaults.AuthenticationScheme)
    .AddAuthioJwtBearer(builder.Configuration.GetSection("Authio"))
    .AddAuthioJwtBearer("Partners", builder.Configuration.GetSection("AuthioPartners"), null);

app.MapGet("/catalog", List).RequireAuthorization(policy => policy
    .AddAuthenticationSchemes(AuthioJwtBearerDefaults.AuthenticationScheme, "Partners")
    .RequireAuthenticatedUser());

Only the default scheme binds its section into the shared AuthioOptions. The Authio client — and with it introspection and on-behalf-of — stays on that one realm, so Introspection other than None on a scheme of another realm stops the application at startup.

Audiences

Authio sets aud to the RFC 8707 resource identifier when the client asked for a registered resource server (resource=https://orders.example.com on the authorize or token request), and otherwise to the audience configured on the client that requested it, its own client id unless changed: that token is addressed to the client, not to an API. So register your API as a resource server in Authio and accept its identifier:

"Audiences": [ "https://orders.example.com" ]

The application that calls the API has to ask Authio for a token for that resource: resource=https://orders.example.com on its authorize or token request, which with Authio.AspNetCore.Bff is Authio:Bff:Resource for the session's token, or Resource on a remote API or typed client (exchanged for it). A token without it is addressed to the client that requested it, and the API refuses it with 401:

WWW-Authenticate: Bearer error="invalid_token", error_description="The token is not addressed to this API: its audience is not 'https://orders.example.com'. Request the token for that resource (RFC 8707)."

The token's own audience is named too when the validation reports it, as IntrospectionOnly does.

Options

Key (Authio:) Default Meaning
Audiences — (required unless Resource) Accepted audiences
Resource — The API's own resource identifier: accepted as an audience, and the resource of its RFC 9728 metadata
PublishResourceMetadata false resource_metadata in 401 challenges (needs Resource); map the document with app.MapAuthioProtectedResourceMetadata()
ResourceScopes, ResourceName, ResourceDocumentation — scopes_supported, resource_name and resource_documentation of the metadata
ClientEncryptionKey / ClientEncryptionKeyPath — The client's private encryption key, for access tokens Authio encrypts to it
AllowLegacyEncryptionAlgorithms false Decrypt JWEs wrapped with RSA1_5
Introspection None None, ActiveCheck or IntrospectionOnly
IntrospectionCacheDuration 00:00:30 How long an active answer is reused (never past exp); 0 disables
ClockSkew 00:00:30 Lifetime tolerance
AccessTokenSignedResponseAlg asymmetric list, never HS* The client's access token algorithm(s); HS* with ClientSecret
RequireHttpsMetadata Authio:RequireHttps HTTPS for discovery and keys
MapAuthorizationClaims true Add role / permission claims
RejectNonAccessTokens true Refuse tokens whose typ is not Bearer
SaveToken true Keep the token in the authentication properties

Anything else of JwtBearerOptions can be changed with services.Configure<JwtBearerOptions>(scheme, ...); an OnTokenValidated you set there runs after the Authio step.

Introspection

A JWT stays valid until it expires even after a logout, a revocation, a suspension or a withdrawn delegation. Authio's back-channel logout tells the client a person signed in to, not the APIs that accept its tokens, so two modes ask Authio's introspection endpoint instead. Both need services.AddAuthioClient(...) with a confidential client allowed to introspect the API's tokens (in Authio, the client the resource server names as its introspection client): Authio:ClientId and Authio:ClientSecret, or, with Authio:TokenEndpointAuthMethod = PrivateKeyJwt, its private key (Authio:ClientPrivateKeyPath or Authio:ClientPrivateKey, plus Authio:ClientKeyId) so no secret is sent; see Authio.Client. On-behalf-of and the client-credentials handler authenticate the same way.

  • ActiveCheck: after local validation, the token must be active. Active answers are cached per jti for IntrospectionCacheDuration, which bounds how long a revoked token is still accepted. If Authio cannot be reached the request is refused (fail closed).
  • IntrospectionOnly: for reference tokens, and for the HMAC-signed tokens of a client whose secret this API does not hold (the client's own API validates them locally, see Access token algorithm). (An encrypted access token no longer needs it: with Authio:ClientEncryptionKey it is decrypted and validated locally, see Encrypted access tokens.) The token is not read locally; the answer must be active, from the configured issuer, for an accepted audience and not expired. The principal is built from the answer (sub, azp from client_id, scope, preferred_username from username, aud, jti, exp…) plus the claims Authio adds to an active answer: the *_resource_access role maps, acr, amr, org_id/org_alias, act, sid and typ. Every requirement works as it does with local validation, and token_type is checked like typ, so refresh and ID tokens are refused. Needs an Authio server that writes those claims (October 2026 onwards); an older one answers without them, and role, permission, sign-in and organization requirements then refuse.
"Authio": {
  "Authority": "https://auth.example.com",
  "Realm": "acme",
  "ClientId": "orders-api",
  "ClientSecret": "<secret>",
  "Audiences": [ "https://orders.example.com" ],
  "Introspection": "ActiveCheck"
}
builder.Services.AddAuthioClient(builder.Configuration.GetSection("Authio"));
builder.Services
    .AddAuthentication(AuthioJwtBearerDefaults.AuthenticationScheme)
    .AddAuthioJwtBearer(builder.Configuration.GetSection("Authio"));
Who can introspect a token

Authio answers an introspection request according to who asks, as RFC 7662 lets an authorization server do (§2.2: a protected resource that "is not allowed to introspect this particular token" gets active: false; §4: the server authorizes which protected resources may introspect):

The token was requested… Its aud Introspected by the API's client (the resource server's introspection client) Introspected by the client that requested it
with resource=https://orders.example.com the resource active: true active: false, by design
without a resource the requesting client active: false active: true
  • The API is the token's audience, so the API is the one that introspects it: that is what ActiveCheck and IntrospectionOnly do, with the client the resource server names in Authio.
  • To the client that requested it, an access token for a resource is opaque (RFC 6749 §1.4): the client sends it and does not read it. Its own introspection answers active: false, which says nothing about whether the API accepts the token. A worker that wants to see its token introspected asks for one without a resource.
  • An active: false answer is refused with 401 and error_description="Authio reports the token as not active to this API's client. …": it was revoked, it expired, its session ended, or the API's Authio:ClientId is not the introspection client of the token's resource server.

Encrypted access tokens

A token encrypted to the client's own key is a nested JWE: the bearer handler decrypts it with Authio:ClientEncryptionKey (the private JWK Authio hands over, or a PEM; or ClientEncryptionKeyPath, read again when the file changes) and validates the signed token inside like any other. Key management RSA-OAEP and RSA-OAEP-256, content A128CBC-HS256, A192CBC-HS384 or A256CBC-HS512; RSA1_5 only with Authio:AllowLegacyEncryptionAlgorithms = true. A token no key opens, or one with a refused algorithm, answers 401 with an error_description saying so.

Protected resource metadata (RFC 9728)

{ "Authio": { "Resource": "https://orders.example.com", "PublishResourceMetadata": true, "ResourceScopes": [ "orders:read" ] } }
app.MapAuthioProtectedResourceMetadata();   // GET /.well-known/oauth-protected-resource

The document names the resource, the realm's issuer as its authorization server and header as the bearer method; every 401 answers WWW-Authenticate: Bearer resource_metadata="https://orders.example.com/.well-known/oauth-protected-resource", …, so a client (an MCP client, for one) discovers where to sign in and which resource to ask for.

The caller

app.MapGet("/me", (HttpContext context) =>
{
    var user = context.GetAuthioUser();          // or User.GetAuthioUser() in a controller
    var token = context.GetAuthioAccessToken();  // the raw bearer token
    var access = context.GetAuthioResourceAccess(); // roles model, parsed once per request
    return user;
}).RequireAuthorization();

AuthioUser holds Kind (User, Client, Consumer or AccountCredential), CredentialForm (client_secret, personal_access_token, api_key or consumer_api_key, for machines), ConsumerName, CredentialId, CredentialName, OwnerUserId / OwnerUsername (who a personal access token or an account credential belongs to; read from owner_username, or from the consumer_name older Authio servers wrote it to), CredentialRealm / CredentialClient (what an account credential is limited to), Impersonated + Impersonator (Subject, Name), Delegated + DelegationId + ActorChain (the clients acting for the user, outermost first), Subject, ClientId (azp), SessionId (sid), TokenId (jti), Audiences, Scopes, Roles, Permissions (resource → methods), ResourceAccess (with the source of each role), OrganizationId / OrganizationAlias, AccountId, Acr / AuthenticationLevel, AuthenticationMethods (amr), IdentityProvider (idp), AuthenticatedAt, Actor (the raw act chain), Email, EmailVerified, Name and PreferredUsername. Every Authio library derives these the same way (conformance/callers.json).

Roles and permissions are only in tokens issued with the roles scope. Other principal helpers: GetAuthioScopes(), HasAuthioScope(scope), HasAuthioRole(role), HasAuthioPermission(resource, method), GetAuthioAuthenticationLevel(), GetAuthioAuthenticationMethods(), GetAuthioActor().

For Authio principals that come from another scheme (for example a cookie session signed in with OpenID Connect), services.AddAuthioClaimsTransformation() adds the same role / permission claims; it is idempotent.

Authorization

Every requirement is available three ways.

// Policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("orders.write", policy => policy
        .RequireAuthioScope("orders:write")
        .RequireAuthioPermission("orders", "write"));
});

// Minimal APIs (endpoints and route groups)
app.MapGet("/orders", List).RequireAuthioScope("orders:read", "orders:admin");
app.MapGroup("/admin").RequireAuthioRole("admin").RequireAuthioAuthenticationMethod("mfa");

// Controllers (needs services.AddAuthioAuthorization())
[AuthioScope("orders:read")]
[AuthioPermission("orders", "write")]
public IActionResult Create() => ...;
Requirement Helper / attribute Passes when
Scope RequireAuthioScope([match,] scopes) / [AuthioScope(..., RequireAll = true)] any (default) or all scopes are in scope, compared exactly
Role RequireAuthioRole([match,] [sources,] roles) / [AuthioRole(..., RequireAll = true, Sources = ...)] any or all roles are keys of a *_resource_access claim (or role claims), case-insensitive; with sources, only roles from those claims count (Client for client_resource_access, Realm, User, Plan, Consumer, Secret, System)
Permission RequireAuthioPermission(resource, method[, sources]) / [AuthioPermission(..., Sources = ...)] Authio's ResourceAccessRules.Grants: any role lists the method under the resource, case-insensitive; with sources, only roles from those claims count (e.g. System to reserve an action for platform roles)
Organization RequireAuthioOrganization(organization?) / [AuthioOrganization] org_id is present; with an argument, org_alias or org_id equals it (case-insensitive)
Step-up level RequireAuthioAuthenticationLevel(min[, maxAge]) / [AuthioAuthenticationLevel(min, MaxAgeSeconds = …)] numeric acr ≥ min (and, with maxAge, the sign-in at most that old)
Step-up recency RequireAuthioRecentAuthentication(maxAge) / [AuthioRecentAuthentication(seconds)] auth_time is at most maxAge ago
Step-up method RequireAuthioAuthenticationMethod([match,] methods) / [AuthioAuthenticationMethod(..., RequireAll = true)] any or all methods are in amr (pwd, otp, swk, mca, fed, mfa…); fed is a sign-in through an external identity provider, counted as one factor, and comes with an idp claim holding the provider's alias (google, github)
Client RequireAuthioClient(clientIds) / [AuthioClient] azp is one of the clients
Caller kind RequireAuthioCallerKind(kinds) / [AuthioCallerKind(AuthioCallerKind.User)] GetAuthioUser().Kind is one of the kinds

Several attributes on one action all apply. The attributes generate policy names (Authio:scope.any:...) that the AuthioAuthorizationPolicyProvider registered by AddAuthioAuthorization() turns into policies; every other policy name falls back to the policies you registered.

Refusals

With AddAuthioAuthorization():

  • a missing scope answers 403 with WWW-Authenticate: Bearer error="insufficient_scope", error_description="...", scope="orders:read orders:write" (RFC 6750 §3.1);
  • a failed step-up (level, method or recency) answers 401 with WWW-Authenticate: Bearer error="insufficient_user_authentication", error_description="...", acr_values="2 3", max_age=300 (RFC 9470 §3): acr_values lists the levels that would do (from the minimum level, or the level the methods bring: otp/mfa/mca 2, swk/hwk/user 3) and max_age the recency asked for. The client signs the user in again with them (IAuthioLoginClient.CreateStepUpAuthorizationAsync) and still checks the new token, because Authio issues the level the sign-in actually reached. With WriteBearerErrors = false the refusal stays a plain 403;
  • other refusals are a plain 403; an invalid or missing token is the bearer handler's 401 with error="invalid_token" and an error_description that says why: the bearer handler's own (expired, signature, issuer) or Authio's, for a token addressed to another audience, one that is not an access token, or one introspection reports as not active or cannot confirm. A refusal your own OnTokenValidated makes keeps its message out of the header; JwtBearerOptions.IncludeErrorDetails = false leaves every description out.
builder.Services.AddAuthioAuthorization(options =>
{
    options.WriteBearerErrors = true;     // default
    options.WriteProblemDetails = true;   // application/problem+json body on 401/403 (default false)
});

Webhooks

"Authio": {
  "Webhooks": {
    "Secret": "whsec_...",
    "Secrets": [ "whsec_previous..." ],
    "Tolerance": "00:05:00",
    "Deduplication": "Memory",
    "DeduplicationWindow": "1.00:00:00",
    "MaxBodySize": 1048576
  }
}
builder.Services
    .AddAuthioWebhooks(builder.Configuration.GetSection("Authio:Webhooks"))
    .AddHandler<AuditTrailHandler>()                              // IAuthioWebhookHandler: every event
    .AddHandler<AuthioWebhookUserData, UserSyncHandler>()         // IAuthioWebhookHandler<AuthioWebhookUserData>
    .On(AuthioWebhookEventTypes.KeysRotated, (evt, services, ct) => ...)
    .On<AuthioWebhookTokenRevokedData>(AuthioWebhookEventTypes.TokenRevoked, (evt, data, services, ct) => ...)
    .OnAny((evt, services, ct) => ...);

app.MapAuthioWebhooks();                 // POST /webhooks/authio, anonymous (the signature authenticates)

The receiver reads the raw body, verifies X-Authio-Signature (HMAC-SHA256 over {t}.{body} with the full whsec_ secret, constant-time) against every configured secret, so a rotation can list the old and new secrets, checks the timestamp against Tolerance, parses the event and skips an X-Authio-Event-Id it has already handled.

Answer When
200 handled, a duplicate, or ping
400 signed, but not an Authio event
401 missing, wrong or stale signature
413 body over MaxBodySize
500 a handler threw; the event id is forgotten so Authio's retry is handled again

Typed handlers receive the data shape of their events: AuthioWebhookUserData (user_created, user_updated, user_deleted), AuthioWebhookLoginData (login_succeeded, login_failed), AuthioWebhookUserLockedOutData, AuthioWebhookTokenRevokedData, AuthioWebhookKeysRotatedData, AuthioWebhookTokenReuseDetectedData, AuthioWebhookAdminResourceData (admin_resource_*), AuthioWebhookUserSuspensionData (user_suspended, user_suspension_ended) and AuthioWebhookPingData. Handlers run before Authio gets its answer: keep them short and enqueue long work. De-duplication is Memory (one instance), Distributed (register an IDistributedCache; the check and the write are two calls) or None; replace IAuthioWebhookDeduplicationStore for anything else.

Downstream calls

// Same audience downstream: forward the caller's token as is.
builder.Services.AddHttpClient<ProfileApi>(c => c.BaseAddress = new("https://profile.example.com"))
    .AddAuthioTokenForwarding();

// Another API, as the user: token exchange (RFC 8693). The delegated token keeps the user as sub,
// names this API in act, and has the downstream resource as aud.
builder.Services.AddAuthioClient(builder.Configuration.GetSection("Authio"));
builder.Services.AddHttpClient<BillingApi>(c => c.BaseAddress = new("https://billing.example.com"))
    .AddAuthioOnBehalfOf(options =>
    {
        options.Resource = "https://billing.example.com";
        options.Scopes = ["invoices:read"];
    });

// Another API, as this API itself (no user): client credentials, from Authio.Client.
builder.Services.AddHttpClient<ReportsApi>(c => c.BaseAddress = new("https://reports.example.com"))
    .AddAuthioClientCredentials(options => options.Resource = "https://reports.example.com");

On-behalf-of needs AddAuthioClient with this API's confidential client, allowed to exchange tokens in Authio. The delegated token is cached per incoming token (jti), resource and scopes until Authio:TokenRefreshSkew before it expires (concurrent requests share one exchange); on a downstream 401 it is dropped and the call retried once. IAuthioOnBehalfOfTokenCache can also be used directly.

Testing

The Authio.Testing package is an in-memory Authio (discovery, JWKS, every grant, introspection, JWE to a client's key…); route the Authio named client to it with services.AddFakeAuthioBackchannel(fake) and mint tokens with fake.Tokens.CreateAccessToken(...).WithAudience(...) (WithAuthTime, WithActor, WithImpersonator, …).

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Authio.AspNetCore:

Package Downloads
Authio.Mcp.AspNetCore

Protects an ASP.NET Core MCP server (OAuth 2.1 resource server) with Authio, on top of Authio.AspNetCore: JWT, JWE and opaque token validation, RFC 9728 protected resource metadata and the MCP-ready WWW-Authenticate challenge, scope / role / permission / caller-kind requirements per tool with HTTP 403 and step-up 401 challenges, and the typed caller.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.0 46 10/14/2026
0.8.1 60 10/14/2026
0.8.0 52 10/14/2026
0.7.0 64 10/13/2026
0.6.0 103 10/9/2026
0.5.1 95 10/8/2026
0.5.0 88 10/8/2026
0.4.0 93 10/6/2026
0.3.0 105 10/5/2026
0.2.0 91 10/4/2026
0.1.0 93 10/4/2026