Authio.Mcp.AspNetCore 0.5.0

dotnet add package Authio.Mcp.AspNetCore --version 0.5.0
                    
NuGet\Install-Package Authio.Mcp.AspNetCore -Version 0.5.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.Mcp.AspNetCore" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Authio.Mcp.AspNetCore" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Authio.Mcp.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.Mcp.AspNetCore --version 0.5.0
                    
#r "nuget: Authio.Mcp.AspNetCore, 0.5.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.Mcp.AspNetCore@0.5.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.Mcp.AspNetCore&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Authio.Mcp.AspNetCore&version=0.5.0
                    
Install as a Cake Tool

Authio.Mcp.AspNetCore

Protect an ASP.NET Core MCP server with Authio.

The MCP server is an OAuth 2.1 resource server; Authio is its authorization server. This package is the MCP layer on top of Authio.AspNetCore, which does the token work:

  • Token validation (from Authio.AspNetCore) — JWT bearer: signature with the client's access token algorithm (AccessTokenSignedResponseAlg; left out, the realm's asymmetric keys, never HS*), issuer, audience (your resource identifier), expiry and token type (header typ at+jwt, RFC 9068; ID and refresh tokens are refused). Nested JWE encrypted to your client is decrypted with its key, within the alg and enc the client is registered for. Introspection (RFC 7662), when asked for, checks every token with Authio (ActiveCheck), or reads opaque tokens entirely from Authio (IntrospectionOnly).
  • The caller (from Authio.AspNetCore) — HttpContext.GetAuthioUser(): whether a person, a client, a consumer or an account credential is calling, roles, permissions, scopes, resource access, organization, how the person signed in (acr/amr), delegation and impersonation.
  • Protected resource metadata (RFC 9728) — always published, so every 401 carries resource_metadata, as the MCP authorization specification requires.
  • Authorization per tool — put [AuthioScope], [AuthioRole], [AuthioPermission], [AuthioCallerKind], [AuthioOrganization], [AuthioClient] or a step-up attribute on a tool. A tools/call the caller may not make is answered at the HTTP layer, before the tool runs: 403 with insufficient_scope and the missing scope (the MCP scope challenge), 403 for a role, permission or caller kind, or 401 with acr_values/max_age and resource_metadata for a step-up (RFC 9470). tools/list only lists the tools the caller may call.
  • Browser MCP clients — a CORS policy for the MCP Inspector and the like.

Connected accounts (trading the caller's token for the provider token of an account they connected) and the server's own client authentication (its TokenEndpointAuthMethod: client_secret_basic, client_secret_post, private_key_jwt or none) come from Authio.Client, registered for you.

Configure it the way the resource server and its client are registered in Authio. The package assumes nothing the Authio console configures: each setting is an option, and a setting left out is off, as in NextAuth — no scope required, no introspection, nothing decrypted. What stays fixed is what the MCP authorization specification requires (see What the MCP specification fixes).

Targets .NET 8 and .NET 10, with the official C# MCP SDK (ModelContextProtocol.AspNetCore).

Install

dotnet add package Authio.Mcp.AspNetCore

It brings Authio.AspNetCore, Authio.Client and ModelContextProtocol.AspNetCore with it.

Register the server in Authio

In the Authio console, in your realm:

  1. Create a resource server whose identifier is the public URL of your MCP endpoint, e.g. https://mcp.example.com/mcp, with a scope such as mcp:tools, and list the MCP clients that call you under Authorized clients.
  2. Create a confidential client for the MCP server itself and set it as the resource server's introspection client — Authio reports a token addressed to the resource server as active only to that client. Its id goes in ClientId; its Token endpoint auth method goes in TokenEndpointAuthMethod, with a secret (ClientSecret) or, for private_key_jwt, the private key (register its public key on the client's Security step).
  3. For connected accounts, give that client the token exchange grant, add a connection provider (e.g. alias github) and allow the MCP clients to use it.

Configure

Every setting is read from Authio:Mcp, under the names every Authio library shares (conformance/options.md). Keep secrets in user-secrets or environment variables (Authio__Mcp__ClientSecret).

{
  "Authio": {
    "Mcp": {
      "Authority": "https://auth.example.com",
      "Realm": "demo",
      "Resource": "https://mcp.example.com/mcp",
      "ClientId": "mcp-server",
      "TokenEndpointAuthMethod": "Basic",
      "ClientSecret": "<secret>",
      "RequiredScopes": [ "mcp:tools" ],
      "Introspection": "ActiveCheck",
      "ResourceName": "My MCP server"
    }
  }
}
Setting Left out Meaning
Authority + Realm, or Issuer Required Where the realm is: the issuer is {Authority}/realms/{Realm}, or Issuer when set. It must match what Authio publishes.
Resource Required The public URL of the MCP endpoint: the resource identifier, the token's aud, and resource in the metadata.
Audiences None Other audiences accepted besides Resource.
ClientId None; required with Introspection, a client credential or TokenEndpointAuthMethod The resource server's introspection client, also the client of the connected-accounts exchange. A server that validates locally and exchanges nothing needs none.
TokenEndpointAuthMethod Follows the credentials The client's Token endpoint auth method, which Authio matches exactly: Basic (client_secret_basic), Post (client_secret_post), PrivateKeyJwt (private_key_jwt) or None. Left out, Basic with a secret, PrivateKeyJwt with only a private key, None with neither.
ClientSecret None The client's secret, with Basic and Post. AuthioOptions.ClientSecretProvider (in code) reads a rotated secret on every use.
ClientPrivateKey / ClientPrivateKeyPath None With PrivateKeyJwt: the client's private key in PEM (PKCS#8, PKCS#1 or SEC1), or the path of the file holding it (re-read when it changes).
ClientKeyId None The kid the client assertion names.
ClientSigningAlgorithm The one the key implies The assertion's algorithm, RS/PS/ES 256/384/512.
AccessTokenSignedResponseAlg RS256/384/512, PS256/384/512, ES256/384/512, never HS* The client's access token signing algorithm in Authio, one value ("RS256") or a list. HS256, HS384 and HS512 are verified with ClientSecret; a server without the secret uses Introspection: IntrospectionOnly instead.
ClientEncryptionKey / ClientEncryptionKeyPath Nothing decrypted The private half of the client's encryption key (JWK or PEM), for access tokens Authio encrypts to it.
EncryptionAlgorithms / EncryptionMethods RSA-OAEP, RSA-OAEP-256; the CBC-HS methods The JWE alg and enc accepted, as registered on the client. A128GCM, A192GCM and A256GCM only when listed.
AllowLegacyEncryptionAlgorithms false Accept JWEs wrapped with RSA1_5.
Introspection None: local validation only ActiveCheck checks a token that passes local validation with Authio, so a revoked token or an ended session is refused before it expires; IntrospectionOnly reads opaque tokens (a client whose access token format is Reference) from Authio's answer. Both need the introspection client.
IntrospectionCacheDuration 00:00:30 How long an active answer is reused, never past the token's expiry; 00:00:00 turns the cache off.
RequiredScopes No scope required Scopes every token must carry, all of them, to reach the endpoint. List the resource server's scopes this server insists on, such as mcp:tools.
ResourceScopes RequiredScopes scopes_supported in the metadata; left out when both are empty.
ResourceName / ResourceDocumentation None resource_name and resource_documentation in the metadata.
ClockSkew, RejectNonAccessTokens, RequireHttps, DiscoveryCacheDuration, HttpTimeout As in the SDK As in Authio.AspNetCore and Authio.Client.

A missing or invalid setting stops the application at start-up with the setting named. In code, the settings are AuthioOptions (realm and client), AuthioJwtBearerOptions named AuthioJwtBearerDefaults.AuthenticationScheme (validation and metadata) and AuthioMcpOptions (required scopes):

builder.Services.AddAuthentication().AddAuthioMcp(options => options.RequiredScopes = ["mcp:tools"]);
builder.Services.Configure<AuthioOptions>(options =>
{
    options.TokenEndpointAuthMethod = AuthioTokenEndpointAuthMethod.PrivateKeyJwt;
    options.ClientSigningCredentials = credentialsFromVault;
});
builder.Services.Configure<AuthioJwtBearerOptions>(AuthioJwtBearerDefaults.AuthenticationScheme, options =>
    options.Introspection = AuthioIntrospectionMode.IntrospectionOnly);

AddAuthioMcp(IConfiguration) reads another section instead of Authio:Mcp.

What the MCP specification fixes

Three things are not settings, because the MCP authorization specification requires them rather than any client or resource server setting in Authio:

Fixed Why
authorization_servers is [issuer], the realm's issuer The specification requires the protected resource metadata to name the authorization server an MCP client must use. For an Authio-protected server that is the realm that issues its tokens; any other value would send the client to a server whose tokens are refused (iss must be the realm).
bearer_methods_supported is ["header"] The specification requires MCP clients to send the token in the Authorization: Bearer header and forbids it in the query string; the package reads only the header. Authio has no other method to configure (no DPoP, no mTLS).
Every 401 carries resource_metadata The specification requires the server to point the client at its protected resource metadata through WWW-Authenticate when it answers 401; that is how the client discovers Authio. The metadata is therefore always published, and 403 insufficient_scope carries it too.

Use with the official C# MCP SDK

using Authio.AspNetCore.Extensions;
using Authio.Mcp.AspNetCore.Defaults;
using Authio.Mcp.AspNetCore.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddAuthioMcp();
builder.Services.AddCors(cors => cors.AddAuthioMcpPolicy());
builder.Services.AddHttpContextAccessor();
builder.Services
    .AddMcpServer()
    .WithHttpTransport(transport => transport.Stateless = true)
    .AddAuthorizationFilters()
    .WithToolsFromAssembly();

var app = builder.Build();

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

app.MapAuthioProtectedResourceMetadata().RequireCors(AuthioMcpDefaults.CorsPolicy);
app.MapMcp("/mcp")
    .RequireAuthioMcp()
    .RequireCors(AuthioMcpDefaults.CorsPolicy);

app.Run();
  • AddAuthioMcp() registers Authio.AspNetCore's bearer scheme (AuthioJwtBearerDefaults.AuthenticationScheme), the Authio client, the Authio policy provider and result handler (AddAuthioAuthorization), and the AuthioMcpDefaults.Policy policy (RequiredScopes; with none, any authenticated caller).
  • MapAuthioProtectedResourceMetadata() (from Authio.AspNetCore) serves the metadata at /.well-known/oauth-protected-resource{path of Resource} — /.well-known/oauth-protected-resource/mcp here.
  • RequireAuthioMcp() requires AuthioMcpDefaults.Policy on the MCP endpoint and authorizes each tools/call against its tool. AddAuthorizationFilters() (from the MCP SDK) filters tools/list with the same attributes.
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [ "https://auth.example.com/realms/demo" ],
  "bearer_methods_supported": [ "header" ],
  "scopes_supported": [ "mcp:tools" ],
  "resource_name": "My MCP server"
}

Authorization per tool

using System.ComponentModel;
using Authio.AspNetCore.Attributes;
using Authio.AspNetCore.Enums;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class FileTools
{
    [McpServerTool(Name = "write_file")]
    [Description("Writes a file.")]
    [AuthioScope("files:write")]                       // 403 insufficient_scope, scope="files:write"
    public static string Write(string path, string content) => "…";

    [McpServerTool(Name = "purge")]
    [Description("Purges the bucket.")]
    [AuthioRole("admin")]                              // 403
    [AuthioRecentAuthentication(300)]                  // 401 insufficient_user_authentication, max_age=300
    public static string Purge() => "…";

    [McpServerTool(Name = "sync")]
    [Description("Runs the nightly sync.")]
    [AuthioCallerKind(AuthioCallerKind.Client)]        // machines only
    public static string Sync() => "…";
}

[AuthioPermission("invoice", "write")], [AuthioOrganization], [AuthioClient("…")], [AuthioAuthenticationLevel(2)], [AuthioAuthenticationMethod("hwk")] and [Authorize(Policy = "…")] work the same way. For the whole endpoint, chain the requirement after RequireAuthioMcp():

app.MapMcp("/mcp").RequireAuthioMcp().RequireAuthioAuthenticationLevel(2, TimeSpan.FromHours(1));

A tool registered only for one session (ConfigureSessionOptions) is not known to RequireAuthioMcp(); the MCP SDK's filter still refuses it, with a JSON-RPC error.

A tool that reads the caller and uses a connected account

using System.ComponentModel;
using Authio.AspNetCore.Extensions;
using Authio.Client.Clients;
using Authio.Client.Models;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class GitHubTools
{
    [McpServerTool(Name = "github_account", ReadOnly = true)]
    [Description("Shows which GitHub account the caller connected through Authio.")]
    public static async Task<string> GitHubAccount(
        IHttpContextAccessor accessor,
        IAuthioTokenClient tokens,
        [Description("Which connected account to use, when there are several.")] string? connectionId = null,
        CancellationToken cancellationToken = default)
    {
        var context = accessor.HttpContext!;
        var user = context.GetAuthioUser()!;                // kind, subject, client, scopes, roles, org, acr/amr, act
        var subjectToken = context.GetAuthioAccessToken()!; // the verified Authio access token

        var result = await tokens.ExchangeForConnectionAsync(subjectToken, "github", connectionId, cancellationToken);

        return result switch
        {
            AuthioConnectionGranted granted => $"{user.Subject} is {granted.ExternalAccount?.Login} on GitHub.",
            AuthioConnectionRequired required => $"Connect your GitHub account first: {required.ConnectUri}",
            AuthioConnectionSelectionRequired selection =>
                "Pick one: " + string.Join(", ", selection.Connections.Select(c => $"{c.Id} ({c.ExternalLogin})")),
            AuthioConnectionRefused refused => $"Authio refused ({refused.Error}, {refused.ErrorCode}): {refused.ErrorDescription}",
            _ => "Unexpected result."
        };
    }
}

Return each refusal to the model as text the person can act on.

A runnable version lives in samples/Authio.Mcp.Sample, with one settings file per client authentication method; its README walks through the Authio console, running it, and connecting the MCP Inspector or Claude Code.

Challenges

Situation Status WWW-Authenticate
No token 401 Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"
Invalid, expired, foreign-audience or undecryptable token; revoked, with Introspection 401 Bearer resource_metadata="…", error="invalid_token", error_description="…", scope="mcp:tools"
Token without one of RequiredScopes, or without a tool's scope 403 Bearer error="insufficient_scope", error_description="…", scope="…", resource_metadata="…"
A tool's role, permission, caller kind, organization or client not met 403 —
A sign-in too old or too weak (endpoint or tool) 401 Bearer error="insufficient_user_authentication", error_description="…", acr_values="…", max_age=…, resource_metadata="…"
With Introspection, Authio refuses this server's client (invalid_client, unauthorized_client) 500 —
With Introspection, Authio cannot be reached, times out or fails (5xx) 503 —

The table is conformance/challenges.json of authio-node, which ChallengeConformanceTests runs through the MCP endpoint; every 401, the step-up one included, names resource_metadata (RFC 9728 §5.1). The 401 for a missing or invalid token also names RequiredScopes as scope, separated by spaces (left out when none is required), so the MCP client asks for them rather than for every scope of scopes_supported. When introspection fails the token is not at fault, so the answer is 500 or 503 with no challenge: an MCP client told invalid_token would sign in again in a loop. The cause is logged.

Browser-based MCP clients (MCP Inspector)

A browser client reads the metadata cross-origin and must read WWW-Authenticate from the 401. Register the CORS policy, call app.UseCors() before app.UseAuthentication(), and add .RequireCors(AuthioMcpDefaults.CorsPolicy) to both the metadata and the MCP endpoint. The policy allows any origin (bearer tokens travel in a header, not in cookies), the MCP headers (Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID), and exposes WWW-Authenticate and Mcp-Session-Id.

API

Member Purpose
AuthenticationBuilder.AddAuthioMcp(Action<AuthioMcpOptions>?) Reads Authio:Mcp and registers validation, the client, the policies and the result handler.
AuthenticationBuilder.AddAuthioMcp(IConfiguration, Action<AuthioMcpOptions>?) The same, from another section.
IEndpointConventionBuilder.RequireAuthioMcp() Requires AuthioMcpDefaults.Policy and authorizes each tools/call against its tool.
CorsOptions.AddAuthioMcpPolicy(string name = AuthioMcpDefaults.CorsPolicy) CORS policy for browser MCP clients.
AuthioMcpOptions.RequiredScopes Scopes every token must carry; none when left out.
AuthioMcpDefaults.Policy / .CorsPolicy / .ConfigurationSection Names to refer to.

From Authio.AspNetCore and Authio.Client: MapAuthioProtectedResourceMetadata(), GetAuthioUser(), GetAuthioAccessToken(), GetAuthioResourceAccess(), the [Authio…] attributes and RequireAuthio… conventions, IAuthioTokenClient.ExchangeForConnectionAsync, IAuthioIntrospectionClient. Every call to Authio goes through the named HttpClient AuthioDefaults.HttpClientName ("Authio").

Upgrading

  • From 0.3: ClientAuthentication is now TokenEndpointAuthMethod (Authio.Client's AuthioTokenEndpointAuthMethod) and ValidAlgorithms is AccessTokenSignedResponseAlg; RequiredScopes and Introspection are off when left out, so a server that relied on mcp:tools and ActiveCheck sets them; AuthioMcpDefaults.Scope is gone. See CHANGELOG.md.

Conformance

conformance/ holds the vectors every Authio library runs, copied from authio-node (see conformance/SOURCE). The tests run callers.json, the access tokens of tokens.json (signed and JWE) and resource-access.json through the MCP endpoint; conformance/PENDING lists what does not apply here, with the reason.

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

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
0.5.0 106 10/14/2026
0.4.1 92 10/14/2026
0.4.0 99 10/14/2026
0.3.0 91 10/13/2026
0.2.0 95 10/8/2026
0.1.0 98 10/4/2026