Skip to content

Commit 9209b61

Browse files
committed
Update samples
1 parent 0badaf8 commit 9209b61

File tree

3 files changed

+74
-71
lines changed

3 files changed

+74
-71
lines changed

samples/ProtectedMCPClient/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ static async Task Main(string[] args)
1717
// Create the authorization config with HTTP listener
1818
var authConfig = new AuthorizationConfig
1919
{
20-
ClientId = "04f79824-ab56-4511-a7cb-d7deaea92dc0",
20+
ClientId = "6ad97b5f-7a7b-413f-8603-7a3517d4adb8",
2121
Scopes = ["api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"]
2222
}.UseHttpListener(hostname: "localhost", listenPort: 1170);
2323

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,76 @@
1-
using Microsoft.AspNetCore.Authentication;
2-
using Microsoft.Extensions.Options;
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
2+
using Microsoft.IdentityModel.Tokens;
33
using ModelContextProtocol.AspNetCore.Auth;
44
using ProtectedMCPServer.Tools;
55
using System.Net.Http.Headers;
66
using System.Security.Claims;
7-
using System.Text.Encodings.Web;
87

98
var builder = WebApplication.CreateBuilder(args);
109

11-
// Configure authentication to use MCP for challenges
10+
// Define Entra ID (Azure AD) configuration
11+
var tenantId = "a2213e1c-e51e-4304-9a0d-effe57f31655"; // This is the tenant ID from your existing configuration
12+
var instance = "https://login.microsoftonline.com/";
13+
14+
// Configure authentication to use MCP for challenges and Entra ID JWT Bearer for token validation
1215
builder.Services.AddAuthentication(options =>
1316
{
14-
options.DefaultScheme = "Bearer";
17+
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
1518
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; // Use MCP for challenges
1619
})
17-
.AddScheme<AuthenticationSchemeOptions, SimpleAuthHandler>("Bearer", options => { })
20+
.AddJwtBearer(options =>
21+
{
22+
// Configure for Entra ID (Azure AD) token validation
23+
options.Authority = $"{instance}{tenantId}/v2.0";
24+
options.TokenValidationParameters = new TokenValidationParameters
25+
{
26+
// Configure validation parameters for Entra ID tokens
27+
ValidateIssuer = true,
28+
ValidateAudience = true,
29+
ValidateLifetime = true,
30+
ValidateIssuerSigningKey = true,
31+
32+
// Default audience - you should replace this with your actual app/API registration ID
33+
ValidAudience = "167b4284-3f92-4436-92ed-38b38f83ae08",
34+
35+
// This validates that tokens come from your Entra ID tenant
36+
ValidIssuer = $"{instance}{tenantId}/v2.0",
37+
38+
// These claims are used by the app for identity representation
39+
NameClaimType = "name",
40+
RoleClaimType = "roles"
41+
};
42+
43+
// Enable metadata-based issuer key retrieval
44+
options.MetadataAddress = $"{instance}{tenantId}/v2.0/.well-known/openid-configuration";
45+
46+
// Add development mode debug logging for token validation
47+
options.Events = new JwtBearerEvents
48+
{
49+
OnTokenValidated = context =>
50+
{
51+
var name = context.Principal?.Identity?.Name ?? "unknown";
52+
var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
53+
Console.WriteLine($"Token validated for: {name} ({email})");
54+
return Task.CompletedTask;
55+
},
56+
OnAuthenticationFailed = context =>
57+
{
58+
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
59+
return Task.CompletedTask;
60+
},
61+
OnChallenge = context =>
62+
{
63+
Console.WriteLine($"Challenging client to authenticate with Entra ID");
64+
return Task.CompletedTask;
65+
}
66+
};
67+
})
1868
.AddMcp(options =>
1969
{
20-
options.ResourceMetadata.AuthorizationServers.Add(new Uri("https://login.microsoftonline.com/a2213e1c-e51e-4304-9a0d-effe57f31655/v2.0"));
70+
// Configure the MCP authentication with the same Entra ID server
71+
options.ResourceMetadata.AuthorizationServers.Add(new Uri($"{instance}{tenantId}/v2.0"));
2172
options.ResourceMetadata.BearerMethodsSupported.Add("header");
22-
options.ResourceMetadata.ScopesSupported.AddRange(["weather.read", "weather.write"]);
73+
options.ResourceMetadata.ScopesSupported.AddRange(["api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"]);
2374
options.ResourceMetadata.ResourceDocumentation = new Uri("https://docs.example.com/api/weather");
2475
});
2576

@@ -55,67 +106,14 @@
55106
Console.WriteLine("PRM Document URL: http://localhost:7071/.well-known/oauth-protected-resource");
56107

57108
Console.WriteLine();
58-
Console.WriteLine("Testing mode: Server will accept ANY non-empty token for authentication");
109+
Console.WriteLine("Entra ID (Azure AD) JWT token validation is configured");
59110
Console.WriteLine();
60111
Console.WriteLine("To test the server:");
61-
Console.WriteLine("1. Use an MCP client that supports authorization");
62-
Console.WriteLine("2. The server will accept any non-empty token sent by the client");
63-
Console.WriteLine("3. Tokens will be logged to the console for debugging");
112+
Console.WriteLine("1. Use an MCP client that supports OAuth flow with Microsoft Entra ID");
113+
Console.WriteLine("2. The client should obtain a token for audience: api://weather-api");
114+
Console.WriteLine("3. The token should be issued by Microsoft Entra ID tenant: " + tenantId);
115+
Console.WriteLine("4. Include this token in the Authorization header of requests");
64116
Console.WriteLine();
65117
Console.WriteLine("Press Ctrl+C to stop the server");
66118

67119
app.Run("http://localhost:7071/");
68-
69-
// Simple auth handler that accepts any non-empty token for testing
70-
// In a real app, you'd use a JWT handler or other proper authentication
71-
class SimpleAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
72-
{
73-
public SimpleAuthHandler(
74-
IOptionsMonitor<AuthenticationSchemeOptions> options,
75-
ILoggerFactory logger,
76-
UrlEncoder encoder)
77-
: base(options, logger, encoder)
78-
{
79-
}
80-
81-
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
82-
{
83-
// Get the Authorization header
84-
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
85-
{
86-
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing"));
87-
}
88-
89-
// Parse the token
90-
var headerValue = authHeader.ToString();
91-
if (!headerValue.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
92-
{
93-
return Task.FromResult(AuthenticateResult.Fail("Bearer token missing"));
94-
}
95-
96-
var token = headerValue["Bearer ".Length..].Trim();
97-
98-
// Accept any non-empty token for testing purposes
99-
if (string.IsNullOrEmpty(token))
100-
{
101-
return Task.FromResult(AuthenticateResult.Fail("Token cannot be empty"));
102-
}
103-
104-
// Log the received token for debugging
105-
Console.WriteLine($"Received and accepted token: {token}");
106-
107-
// Create a claims identity with required claims
108-
var claims = new[]
109-
{
110-
new Claim(ClaimTypes.Name, "demo_user"),
111-
new Claim(ClaimTypes.NameIdentifier, "user123"),
112-
new Claim("scope", "weather.read")
113-
};
114-
115-
var identity = new ClaimsIdentity(claims, "Bearer");
116-
var principal = new ClaimsPrincipal(identity);
117-
var ticket = new AuthenticationTicket(principal, "Bearer");
118-
119-
return Task.FromResult(AuthenticateResult.Success(ticket));
120-
}
121-
}

src/ModelContextProtocol/Auth/OAuthAuthenticationService.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,15 @@ private void VerifyResourceUri(Uri resourceUri, Uri metadataResourceUri)
165165

166166
private async Task<AuthorizationServerMetadata> DiscoverAuthorizationServerMetadataAsync(Uri authServerUri)
167167
{
168-
// Try common well-known endpoints
169-
var openIdConfigUri = new Uri(authServerUri, ".well-known/openid-configuration");
170-
var oauthConfigUri = new Uri(authServerUri, ".well-known/oauth-authorization-server");
171-
168+
// Ensure the authServerUri ends with a trailing slash
169+
var baseUri = authServerUri.AbsoluteUri.EndsWith("/")
170+
? authServerUri
171+
: new Uri(authServerUri.AbsoluteUri + "/");
172+
173+
// Now combine with the well-known endpoints
174+
var openIdConfigUri = new Uri(baseUri, ".well-known/openid-configuration");
175+
var oauthConfigUri = new Uri(baseUri, ".well-known/oauth-authorization-server");
176+
172177
// Try OpenID Connect configuration endpoint first
173178
try
174179
{

0 commit comments

Comments
 (0)