Authio.Mcp.AspNetCore
0.1.0
See the version list below for details.
dotnet add package Authio.Mcp.AspNetCore --version 0.1.0
NuGet\Install-Package Authio.Mcp.AspNetCore -Version 0.1.0
<PackageReference Include="Authio.Mcp.AspNetCore" Version="0.1.0" />
<PackageVersion Include="Authio.Mcp.AspNetCore" Version="0.1.0" />
<PackageReference Include="Authio.Mcp.AspNetCore" />
paket add Authio.Mcp.AspNetCore --version 0.1.0
#r "nuget: Authio.Mcp.AspNetCore, 0.1.0"
#:package Authio.Mcp.AspNetCore@0.1.0
#addin nuget:?package=Authio.Mcp.AspNetCore&version=0.1.0
#tool nuget:?package=Authio.Mcp.AspNetCore&version=0.1.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 wires the server's half of that contract:
- Token validation — JWT bearer against the realm's published keys: signature (asymmetric algorithms only: RS/PS/ES 256/384/512), issuer, audience (your resource identifier) and expiry.
- Introspection (RFC 7662) — every token that passes local validation is checked with Authio, because only
Authio knows whether it was revoked or its session ended. An active answer is cached per
jtifor 30 seconds. - Protected resource metadata (RFC 9728) — the document an MCP client reads to discover Authio and the scopes to request.
- MCP-ready challenges —
401withWWW-Authenticate: Bearer … resource_metadata="…"and403witherror="insufficient_scope", scope="…". - Connected accounts — trade the caller's Authio token for the provider token of an account they connected (GitHub, Google…) through Authio's token exchange (RFC 8693).
- Start-up checks — the realm's metadata is read when the application starts; a wrong base URL, realm or issuer stops it with a sentence instead of every token being refused later.
Targets .NET 8 and .NET 10. It does not depend on the MCP SDK; it works with any endpoint you protect.
Install
dotnet add package Authio.Mcp.AspNetCore
dotnet add package ModelContextProtocol.AspNetCore
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. - Create a confidential client for the MCP server itself and set it as the resource server's
introspection client. Its id and secret go in
ClientId/ClientSecret. - For connected accounts, add a connection provider (e.g. alias
github) and allow the MCP clients that call you to use it.
Configure
appsettings.json (keep the secret in user-secrets or an environment variable: Authio__Mcp__ClientSecret):
{
"Authio": {
"Mcp": {
"BaseUrl": "https://auth.example.com",
"Realm": "demo",
"ResourceUrl": "https://mcp.example.com/mcp",
"ClientId": "mcp-server",
"ClientSecret": "<secret>",
"RequiredScopes": [ "mcp:tools" ],
"ResourceName": "My MCP server"
}
}
}
| Setting | Meaning |
|---|---|
BaseUrl |
Where Authio answers, without the realm. Must match the base URL Authio publishes (Auth:PublicBaseUrl): the issuer {BaseUrl}/realms/{Realm} is built from it. |
Realm |
The realm the resource server and its clients live in. |
ResourceUrl |
The public URL of the MCP endpoint. It is the resource identifier and the token's aud (scheme and host lower-cased, default port dropped, trailing slash trimmed — as Authio stores it). |
ClientId / ClientSecret |
The resource server's introspection client. Also used for the token exchange. |
RequiredScopes |
Scopes every token must carry. Default: mcp:tools. |
ResourceName |
Optional resource_name in the protected resource metadata. |
IntrospectionCacheDuration |
How long an active introspection answer is trusted. Default: 00:00:30; 00:00:00 disables the cache. |
Every setting can also be set in code: AddAuthioMcp(options => { … }) runs after the configuration binding.
Use with the official C# MCP SDK
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)
.WithToolsFromAssembly();
var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapAuthioProtectedResourceMetadata();
app.MapMcp("/mcp")
.RequireAuthorization(AuthioMcpDefaults.Policy)
.RequireCors(AuthioMcpDefaults.CorsPolicy);
app.Run();
MapAuthioProtectedResourceMetadata() serves the document at /.well-known/oauth-protected-resource{resource path}
— for https://mcp.example.com/mcp that is /.well-known/oauth-protected-resource/mcp:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": [ "https://auth.example.com/realms/demo" ],
"scopes_supported": [ "mcp:tools" ],
"bearer_methods_supported": [ "header" ],
"resource_name": "My MCP server"
}
A tool that reads the caller and uses a connected account
using System.ComponentModel;
using Authio.Mcp.AspNetCore.Exchanges;
using Authio.Mcp.AspNetCore.Extensions;
using Authio.Mcp.AspNetCore.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,
IAuthioConnectionExchange exchange,
[Description("Which connected account to use, when there are several.")] string? connectionId = null,
CancellationToken cancellationToken = default)
{
var context = accessor.HttpContext!;
var caller = context.GetAuthioCaller(); // sub, azp, scopes, org_id, org_alias, jti, exp
var subjectToken = context.GetAuthioAccessToken()!; // the verified Authio access token
var result = await exchange.ExchangeAsync(subjectToken, "github", connectionId, cancellationToken);
return result switch
{
AuthioConnectionGranted granted =>
$"{caller!.Subject} is {granted.ExternalAccount?.Login} on GitHub.", // use granted.AccessToken
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.Code}): {refused.Description}",
_ => "Unexpected result."
};
}
}
Return each refusal to the model as text the person can act on: the address where they connect the account, the accounts to choose from, or the reason Authio gave.
A runnable version lives in samples/Authio.Mcp.Sample.
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 or revoked token | 401 | Bearer error="invalid_token", error_description="…", resource_metadata="…" |
| Token without the required scopes | 403 | Bearer error="insufficient_scope", scope="mcp:tools", resource_metadata="…" |
If Authio refuses the introspection request itself (wrong client credentials), the request fails with a 500 and the
error is logged naming Authio:Mcp:ClientId and Authio:Mcp:ClientSecret.
Browser-based MCP clients (MCP Inspector)
A browser client reads the protected resource metadata cross-origin and must read WWW-Authenticate from the 401
to find it. The metadata endpoint already answers any origin. For the MCP endpoint, register the CORS policy and
add it to the endpoint, with app.UseCors() before app.UseAuthentication():
builder.Services.AddCors(cors => cors.AddAuthioMcpPolicy());
// …
app.UseCors();
app.MapMcp("/mcp").RequireAuthorization(AuthioMcpDefaults.Policy).RequireCors(AuthioMcpDefaults.CorsPolicy);
The policy allows any origin, the MCP headers (Authorization, Content-Type, Mcp-Session-Id,
Mcp-Protocol-Version, Last-Event-ID) and exposes WWW-Authenticate and Mcp-Session-Id. If you use your own
policy, expose WWW-Authenticate in it.
API
| Member | Purpose |
|---|---|
AuthenticationBuilder.AddAuthioMcp(Action<AuthioMcpOptions>?) |
Registers the scheme, the policy, introspection, the exchange and the start-up check. |
IEndpointRouteBuilder.MapAuthioProtectedResourceMetadata() |
Serves the RFC 9728 document. |
CorsOptions.AddAuthioMcpPolicy(string name = AuthioMcpDefaults.CorsPolicy) |
CORS policy for browser MCP clients. |
AuthioMcpDefaults.Policy / .AuthenticationScheme / .CorsPolicy / .HttpClientName |
Names to refer to. |
IAuthioConnectionExchange.ExchangeAsync(subjectToken, provider, connectionId?, ct) |
Connected-accounts token exchange. |
HttpContext.GetAuthioAccessToken() |
The verified raw token (subject token for the exchange). |
HttpContext.GetAuthioCaller() / ClaimsPrincipal.GetAuthioCaller() |
AuthioCaller (subject, client, scopes, organization, token id, expiry). |
ClaimsPrincipal.GetAuthioScopes() / HasAuthioScopes(scopes) |
Scope checks, e.g. per tool. |
IAuthioTokenIntrospector |
Replaceable introspection service. |
All calls to Authio (metadata, keys, introspection, exchange) go through the named HttpClient
AuthioMcpDefaults.HttpClientName, so you can add a proxy, a handler or resilience with
services.AddHttpClient(AuthioMcpDefaults.HttpClientName)….
License
MIT
| 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
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.10)
-
net8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.23)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.