Authio.Mcp.AspNetCore
0.3.0
See the version list below for details.
dotnet add package Authio.Mcp.AspNetCore --version 0.3.0
NuGet\Install-Package Authio.Mcp.AspNetCore -Version 0.3.0
<PackageReference Include="Authio.Mcp.AspNetCore" Version="0.3.0" />
<PackageVersion Include="Authio.Mcp.AspNetCore" Version="0.3.0" />
<PackageReference Include="Authio.Mcp.AspNetCore" />
paket add Authio.Mcp.AspNetCore --version 0.3.0
#r "nuget: Authio.Mcp.AspNetCore, 0.3.0"
#:package Authio.Mcp.AspNetCore@0.3.0
#addin nuget:?package=Authio.Mcp.AspNetCore&version=0.3.0
#tool nuget:?package=Authio.Mcp.AspNetCore&version=0.3.0
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 against the realm's keys: signature (asymmetric algorithms only: RS/PS/ES 256/384/512), issuer, audience (your resource identifier), expiry and token type (ID and refresh tokens are refused). Nested JWE encrypted to your client is decrypted with its key (RSA-OAEP,RSA-OAEP-256;RSA1_5only on request). Introspection (RFC 7662) checks every token with Authio by default (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
401carriesresource_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. Atools/callthe caller may not make is answered at the HTTP layer, before the tool runs:403withinsufficient_scopeand the missing scope (the MCP scope challenge),403for a role, permission or caller kind, or401withacr_values/max_agefor a step-up (RFC 9470).tools/listonly 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 (client_secret_basic, client_secret_post, private_key_jwt) come from Authio.Client,
registered for you.
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:
- 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 asmcp:tools, and list the MCP clients that call you under Authorized clients. - 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; it authenticates with a secret (ClientSecret) or withprivate_key_jwt(register its public key on the client's Security step and give this server the private key). - 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",
"ClientSecret": "<secret>",
"RequiredScopes": [ "mcp:tools" ],
"ResourceName": "My MCP server"
}
}
}
| Setting | Meaning |
|---|---|
Authority + Realm, or Issuer |
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 |
Other audiences accepted besides Resource. |
ClientId |
The resource server's introspection client, also the client of the connected-accounts exchange. |
ClientAuthentication |
Basic (client_secret_basic, default), Post (client_secret_post), PrivateKeyJwt (private_key_jwt) or None. |
ClientSecret |
The client's secret, with Basic and Post. AuthioOptions.ClientSecretProvider (in code) reads a rotated secret on every use. |
ClientPrivateKey / ClientPrivateKeyPath |
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 |
The kid the client assertion names. |
ClientSigningAlgorithm |
The assertion's algorithm, RS/PS/ES 256/384/512; by default the one the key implies. |
ClientEncryptionKey / ClientEncryptionKeyPath |
The private half of the client's encryption key (JWK or PEM), for access tokens Authio encrypts to it. |
AllowLegacyEncryptionAlgorithms |
Accept JWEs wrapped with RSA1_5. Default false. |
Introspection |
ActiveCheck (default here: a token that passes local validation is checked with Authio), IntrospectionOnly (opaque tokens: Authio's answer is the token) or None. |
IntrospectionCacheDuration |
How long an active answer is reused, never past the token's expiry. Default 00:00:30; 00:00:00 turns the cache off. |
RequiredScopes |
Scopes every token must carry to reach the endpoint. Default mcp:tools. |
ResourceScopes |
scopes_supported in the metadata. Default: RequiredScopes. |
ResourceName / ResourceDocumentation |
resource_name and resource_documentation in the metadata. |
ClockSkew, ValidAlgorithms, RejectNonAccessTokens, RequireHttps, DiscoveryCacheDuration, HttpTimeout |
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.ClientSigningCredentials = credentialsFromVault);
builder.Services.Configure<AuthioJwtBearerOptions>(AuthioJwtBearerDefaults.AuthenticationScheme, options =>
options.Introspection = AuthioIntrospectionMode.IntrospectionOnly);
AddAuthioMcp(IConfiguration) reads another section instead of Authio:Mcp.
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()registersAuthio.AspNetCore's bearer scheme (AuthioJwtBearerDefaults.AuthenticationScheme), the Authio client, the Authio policy provider and result handler (AddAuthioAuthorization), and theAuthioMcpDefaults.Policypolicy (RequiredScopes).MapAuthioProtectedResourceMetadata()(fromAuthio.AspNetCore) serves the metadata at/.well-known/oauth-protected-resource{path of Resource}—/.well-known/oauth-protected-resource/mcphere.RequireAuthioMcp()requiresAuthioMcpDefaults.Policyon the MCP endpoint and authorizes eachtools/callagainst its tool.AddAuthorizationFilters()(from the MCP SDK) filterstools/listwith 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" |
| Invalid, expired, foreign-audience, revoked or undecryptable token | 401 | Bearer resource_metadata="…", error="invalid_token", error_description="…" |
Token without 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=… |
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. |
AuthioMcpDefaults.Policy / .CorsPolicy / .ConfigurationSection / .Scope |
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").
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
| Product | Versions 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. |
-
net10.0
- Authio.AspNetCore (>= 0.7.0)
- ModelContextProtocol.AspNetCore (>= 2.2.0)
-
net8.0
- Authio.AspNetCore (>= 0.7.0)
- ModelContextProtocol.AspNetCore (>= 2.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.