Skip to content

WIP: Authorization support #349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c7183f6
Exploratory implementation
localden Apr 23, 2025
389fb3d
Fix JSON reference issues
localden Apr 23, 2025
ddca6cc
Remove error codes - we don't use those
localden Apr 23, 2025
99a417f
Update sample to use proper SSE transport definition
localden Apr 23, 2025
53c1151
Stub for server implementation
localden Apr 23, 2025
3e9462c
HTTP for local testing
localden Apr 23, 2025
ecc40ab
Tinkering with test logic
localden Apr 23, 2025
4c7a578
Iterating on the changes
localden Apr 24, 2025
d339973
Testing client configuration
localden Apr 24, 2025
bdee0e3
Update to make sure naming is consistent
localden Apr 24, 2025
b0d9932
No need to keep track of this
localden Apr 24, 2025
e6c1995
Updated logc
localden Apr 24, 2025
2f44765
Update with proper token logic
localden Apr 24, 2025
bf9f63e
Cleanup of unused declarations
localden Apr 24, 2025
3fd7681
Remove handler from transport definition
localden May 1, 2025
9bf4ea3
Amend middleware logic
localden May 1, 2025
400f191
Merge branch 'main' into localden/auth
localden May 1, 2025
fd60a1c
Trim implementation
localden May 1, 2025
f699f77
Cleanup
localden May 1, 2025
dc8f3a1
Bit more cleanup here.
localden May 1, 2025
7c2e177
Remove test that is no longer relevant
localden May 1, 2025
c88e473
Use URI properly
localden May 2, 2025
4d37991
Functional cleanup
localden May 2, 2025
5489d65
Merge branch 'main' into localden/auth
localden May 2, 2025
084590d
Update ProtectedResourceMetadataTests.cs
localden May 2, 2025
f9f7c9d
Update ProtectedResourceMetadataTests.cs
localden May 2, 2025
3676c0e
Update for consistency
localden May 2, 2025
950a3c4
Cleanup
localden May 2, 2025
e8b3e0d
Cleanup
localden May 2, 2025
e80bad2
Update Program.cs
localden May 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ docs/api

# Rider
.idea/
.idea_modules/
.idea_modules/

# Specs
.specs/
14 changes: 14 additions & 0 deletions ModelContextProtocol.sln
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNet
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore.Tests", "tests\ModelContextProtocol.AspNetCore.Tests\ModelContextProtocol.AspNetCore.Tests.csproj", "{85557BA6-3D29-4C95-A646-2A972B1C2F25}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationExample", "samples\AuthorizationExample\AuthorizationExample.csproj", "{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationServerExample", "samples\AuthorizationServerExample\AuthorizationServerExample.csproj", "{05C500AF-9CF6-C2E7-2782-95271975A5DE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -110,6 +114,14 @@ Global
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.Build.0 = Release|Any CPU
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Release|Any CPU.Build.0 = Release|Any CPU
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -128,6 +140,8 @@ Global
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
{85557BA6-3D29-4C95-A646-2A972B1C2F25} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928}
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{05C500AF-9CF6-C2E7-2782-95271975A5DE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
Expand Down
14 changes: 14 additions & 0 deletions samples/AuthorizationExample/AuthorizationExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
</ItemGroup>

</Project>
92 changes: 92 additions & 0 deletions samples/AuthorizationExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Diagnostics;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Auth;
using ModelContextProtocol.Protocol.Transport;

namespace AuthorizationExample;

/// <summary>
/// Example demonstrating how to use the MCP C# SDK with OAuth authorization.
/// </summary>
public class Program
{
public static async Task Main(string[] args)
{
// Define the MCP server endpoint that requires OAuth authentication
var serverEndpoint = new Uri("http://localhost:7071/sse");

// Configuration values for OAuth redirect
string hostname = "localhost";
int port = 13261;
string callbackPath = "/oauth/callback/";

// Set up the SSE transport with authorization support
var transportOptions = new SseClientTransportOptions
{
Endpoint = serverEndpoint,
AuthorizationOptions = new AuthorizationOptions
{
// Pre-registered client credentials (if applicable)
ClientId = "04f79824-ab56-4511-a7cb-d7deaea92dc0",

// Setting some pre-defined scopes the client requests.
Scopes = ["User.Read"],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a usecase for this to be dynamic. I haven't looked at the rest of the code yet, but please ensure there is support for supplying the scopes dynamically in the server metadata based upon the incoming request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DavidParks8 - this is an example. You can specify whatever scopes you want here when you establish the transport options.


// Specify the exact same redirect URIs that are registered with the OAuth server
RedirectUris = new[]
{
$"http://{hostname}:{port}{callbackPath}"
},
// Configure the authorize callback with the same hostname, port, and path
AuthorizeCallback = AuthorizationService.CreateHttpListenerAuthorizeCallback(
openBrowser: async (url) =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the url could be malicious. There has to be a more secure way to do this than blindly trusting what the server provides.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you hand an un‑validated string off to ShellExecute (UseShellExecute = true) you are effectively letting the user invoke any registered protocol or file association. As soon as Windows sees something like:

• mshta://evil.com/boom.hta
• ms‑msdt:/id PCWDiagnostic /skip force /path "\evil\share\payload.sps" (the “Follina” trick)
• powershell://… (or a custom “evilproto://” that a malicious installer put on the box)
• data:text/html,
• javascript:alert(1)
• file://C:/windows/system32/calc.exe
• C:\somewhere\malicious.exe
• mybad.lnk or anything else with a registered verb

…ShellExecute will hand it off to whatever handler is registered. That means remote HTAs, LNKs that point to EXEs, custom protocols, PowerShell‐URI handlers, the Works.

If all you truly want to do is open an HTTP(S) address (and nothing else), you must allowlist the scheme

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if this is just a sample, we shouldn't be insecure. People are likely to copy these samples into real products.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is really applicable here - this is the client configuration. If you're writing a MCP client, you can set the redirect URI to anything you want when you spin up the listener. Happy to discuss if you think otherwise.

{
Console.WriteLine($"Opening browser to authorize at: {url}");
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
},
hostname: hostname,
listenPort: port,
redirectPath: callbackPath
)
}
};

Console.WriteLine("Connecting to MCP server...");

try
{
// Create the client with authorization-enabled transport
var transport = new SseClientTransport(transportOptions);
var client = await McpClientFactory.CreateAsync(transport);

Console.WriteLine("Successfully connected and authorized!");

// Print the list of tools available from the server.
Console.WriteLine("\nAvailable tools:");
foreach (var tool in await client.ListToolsAsync())
{
Console.WriteLine($" - {tool.Name}: {tool.Description}");
}

// Execute a tool (this would normally be driven by LLM tool invocations).
Console.WriteLine("\nCalling 'echo' tool...");
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
cancellationToken: CancellationToken.None);

// echo always returns one and only one text content object
Console.WriteLine($"Tool response: {result.Content.First(c => c.Type == "text").Text}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner Error: {ex.InnerException.Message}");
}
// Print the stack trace for debugging
Console.WriteLine($"Stack Trace:\n{ex.StackTrace}");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
</ItemGroup>

</Project>
132 changes: 132 additions & 0 deletions samples/AuthorizationServerExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using ModelContextProtocol;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Auth;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server.Auth;

namespace AuthorizationServerExample;

/// <summary>
/// Example demonstrating how to implement authorization in an MCP server.
/// </summary>
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("=== MCP Server with Authorization Support ===");
Console.WriteLine("This example demonstrates how to implement OAuth authorization in an MCP server.");
Console.WriteLine();

var builder = WebApplication.CreateBuilder(args);

// 1. Define the Protected Resource Metadata for the server
// This is the information that will be provided to clients when they need to authenticate
var prm = new ProtectedResourceMetadata
{
Resource = new Uri("http://localhost:7071"), // Changed from HTTPS to HTTP for local development
AuthorizationServers = [ new Uri("https://login.microsoftonline.com/a2213e1c-e51e-4304-9a0d-effe57f31655/v2.0")], // Let's use a dummy Entra ID tenant here
BearerMethodsSupported = ["header"], // We support the Authorization header
ScopesSupported = ["mcp.tools", "mcp.prompts", "mcp.resources"], // Scopes supported by this resource
ResourceDocumentation = new Uri("https://example.com/docs/mcp-server-auth") // Optional documentation URL
};

// 2. Define a token validator function
// This function receives the token from the Authorization header and should validate it
// In a real application, this would verify the token with your identity provider
async Task<bool> ValidateToken(string token)
{
// For demo purposes, we'll accept any token.
return true;
}

// 3. Create an authorization provider with the PRM and token validator
var authProvider = new BasicServerAuthorizationProvider(prm, ValidateToken); // 4. Configure the MCP server with authorization
// WithAuthorization will automatically configure:
// - Authorization provider registration
// - Protected resource metadata endpoint (/.well-known/oauth-protected-resource)
// - Token validation middleware
// - Authorization for all MCP endpoints
builder.Services.AddMcpServer(options =>
{
options.ServerInstructions = "This is an MCP server with OAuth authorization enabled.";

// Configure regular server capabilities like tools, prompts, resources
options.Capabilities = new()
{
Tools = new()
{
// Simple Echo tool

CallToolHandler = (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
{
throw new McpException("Missing required argument 'message'");
}

return new ValueTask<CallToolResponse>(new CallToolResponse()
{
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
});
}

// Protected tool that requires authorization
if (request.Params?.Name == "protected-data")
{
// This tool will only be accessible to authenticated clients
return new ValueTask<CallToolResponse>(new CallToolResponse()
{
Content = [new Content() { Text = "This is protected data that only authorized clients can access" }]
});
}

throw new McpException($"Unknown tool: '{request.Params?.Name}'");
},

ListToolsHandler = async (_, _) => new()
{
Tools =
[
new()
{
Name = "echo",
Description = "Echoes back the message you send"
},
new()
{
Name = "protected-data",
Description = "Returns protected data that requires authorization"
}
]
}
}
};
})
.WithAuthorization(authProvider) // Enable authorization with our provider
.WithHttpTransport(); // Configure HTTP transport

var app = builder.Build();

// 5. Map MCP endpoints
// Note: Authorization is now handled automatically by WithAuthorization()
app.MapMcp();

// Configure the server URL
app.Urls.Add("http://localhost:7071");

Console.WriteLine("Starting MCP server with authorization at http://localhost:7071");
Console.WriteLine("PRM Document URL: http://localhost:7071/.well-known/oauth-protected-resource");

Console.WriteLine();
Console.WriteLine("To test the server:");
Console.WriteLine("1. Use an MCP client that supports authorization");
Console.WriteLine("2. When prompted for authorization, enter 'valid_token' to gain access");
Console.WriteLine("3. Any other token value will be rejected with a 401 Unauthorized");
Console.WriteLine();
Console.WriteLine("Press Ctrl+C to stop the server");

await app.RunAsync();
}
}
12 changes: 12 additions & 0 deletions samples/AuthorizationServerExample/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"AuthorizationServerExample": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:50481;http://localhost:50482"
}
}
}
23 changes: 23 additions & 0 deletions src/ModelContextProtocol.AspNetCore/AuthorizationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Builder;

namespace ModelContextProtocol.AspNetCore;

/// <summary>
/// Extension methods for using MCP authorization in ASP.NET Core applications.
/// </summary>
public static class AuthorizationExtensions
{
/// <summary>
/// Adds MCP authorization middleware to the specified <see cref="IApplicationBuilder"/>, which enables
/// OAuth 2.0 authorization for MCP servers.
///
/// Note: This method is called automatically when using <c>WithAuthorization()</c>, so you typically
/// don't need to call it directly. It's available for advanced scenarios where more control is needed.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IApplicationBuilder UseMcpAuthorization(this IApplicationBuilder builder)
{
return builder.UseMiddleware<AuthorizationMiddleware>();
}
}
Loading
Loading