|
1 | | -using Microsoft.AspNetCore.Authentication; |
2 | | -using Microsoft.Extensions.Options; |
| 1 | +using Microsoft.AspNetCore.Authentication.JwtBearer; |
| 2 | +using Microsoft.IdentityModel.Tokens; |
3 | 3 | using ModelContextProtocol.AspNetCore.Auth; |
4 | 4 | using ProtectedMCPServer.Tools; |
5 | 5 | using System.Net.Http.Headers; |
6 | 6 | using System.Security.Claims; |
7 | | -using System.Text.Encodings.Web; |
8 | 7 |
|
9 | 8 | var builder = WebApplication.CreateBuilder(args); |
10 | 9 |
|
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 |
12 | 15 | builder.Services.AddAuthentication(options => |
13 | 16 | { |
14 | | - options.DefaultScheme = "Bearer"; |
| 17 | + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; |
15 | 18 | options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; // Use MCP for challenges |
16 | 19 | }) |
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 | +}) |
18 | 68 | .AddMcp(options => |
19 | 69 | { |
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")); |
21 | 72 | 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"]); |
23 | 74 | options.ResourceMetadata.ResourceDocumentation = new Uri("https://docs.example.com/api/weather"); |
24 | 75 | }); |
25 | 76 |
|
|
55 | 106 | Console.WriteLine("PRM Document URL: http://localhost:7071/.well-known/oauth-protected-resource"); |
56 | 107 |
|
57 | 108 | 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"); |
59 | 110 | Console.WriteLine(); |
60 | 111 | 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"); |
64 | 116 | Console.WriteLine(); |
65 | 117 | Console.WriteLine("Press Ctrl+C to stop the server"); |
66 | 118 |
|
67 | 119 | 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 | | -} |
0 commit comments