-
Notifications
You must be signed in to change notification settings - Fork 562
An example of using Azure B2C as an Oauth provider for an MCP server.… #617
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
vbitzceo
wants to merge
2
commits into
modelcontextprotocol:main
from
vbitzceo:AzureB2CClientCredentialsFlowMCP
+634
−0
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <UserSecretsId>783daef3-9c45-408d-a1d3-7caf44724f39</UserSecretsId> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" /> | ||
| <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| using Microsoft.AspNetCore.Authentication.JwtBearer; | ||
| using Microsoft.IdentityModel.Tokens; | ||
| using ModelContextProtocol.AspNetCore.Authentication; | ||
| using AzureB2CClientCredentials.Tools; | ||
| using System.Net.Http.Headers; | ||
| using System.Security.Claims; | ||
|
|
||
| var builder = WebApplication.CreateBuilder(args); | ||
|
|
||
| var serverUrl = "http://localhost:7071/"; | ||
|
|
||
| // Azure B2C Configuration for Client Credentials Flow | ||
| // IMPORTANT: Azure B2C requires a policy even for client credentials flow | ||
| // This is different from Azure AD which supports policy-free client credentials | ||
| var azureB2CInstance = builder.Configuration["AzureB2C:Instance"] ?? "https://yourtenant.b2clogin.com"; | ||
| var azureB2CTenant = builder.Configuration["AzureB2C:Tenant"] ?? "yourtenant.onmicrosoft.com"; | ||
| var azureB2CPolicy = builder.Configuration["AzureB2C:Policy"] ?? "B2C_1_signupsignin"; | ||
| var azureB2CClientId = builder.Configuration["AzureB2C:ClientId"] ?? "your-client-id"; | ||
| // Azure B2C requires the policy in the authority URL even for client credentials flow | ||
| var azureB2CAuthority = $"{azureB2CInstance}/{azureB2CTenant}/{azureB2CPolicy}/v2.0"; | ||
| var azureB2CMetadataAddress = $"{azureB2CAuthority}/.well-known/openid-configuration"; | ||
|
|
||
| builder.Services.AddAuthentication(options => | ||
| { | ||
| options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; | ||
| options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | ||
| }) | ||
| .AddJwtBearer(options => | ||
| { | ||
| // Configure to validate tokens from Azure B2C | ||
| options.Authority = azureB2CAuthority; | ||
| options.MetadataAddress = azureB2CMetadataAddress; | ||
| options.TokenValidationParameters = new TokenValidationParameters | ||
| { | ||
| ValidateIssuer = true, | ||
| ValidateAudience = true, | ||
| ValidateLifetime = true, | ||
| ValidateIssuerSigningKey = true, | ||
| ValidAudience = azureB2CClientId, | ||
| ValidIssuer = azureB2CAuthority, | ||
| NameClaimType = ClaimTypes.Name, | ||
| RoleClaimType = ClaimTypes.Role, | ||
| // Azure B2C uses 'aud' claim for audience validation | ||
| ValidAudiences = new[] { azureB2CClientId }, | ||
| // Allow for clock skew | ||
| ClockSkew = TimeSpan.FromMinutes(5) | ||
| }; | ||
|
|
||
| options.Events = new JwtBearerEvents | ||
| { | ||
| OnTokenValidated = context => | ||
| { | ||
| // For client credentials flow, we don't have user claims like email | ||
| // Instead, we have application/service claims | ||
| var clientId = context.Principal?.FindFirstValue("aud") ?? "unknown"; | ||
| var appId = context.Principal?.FindFirstValue("appid") ?? | ||
| context.Principal?.FindFirstValue("azp") ?? "unknown"; | ||
| var objectId = context.Principal?.FindFirstValue("oid") ?? | ||
| context.Principal?.FindFirstValue("sub") ?? "unknown"; | ||
| Console.WriteLine($"Token validated for client: {clientId} - App ID: {appId} - Object ID: {objectId}"); | ||
| return Task.CompletedTask; | ||
| }, | ||
| OnAuthenticationFailed = context => | ||
| { | ||
| Console.WriteLine($"Authentication failed: {context.Exception.Message}"); | ||
| return Task.CompletedTask; | ||
| }, | ||
| OnChallenge = context => | ||
| { | ||
| Console.WriteLine($"Challenging client to authenticate with Azure B2C"); | ||
| return Task.CompletedTask; | ||
| } | ||
| }; | ||
| }) | ||
| .AddMcp(options => | ||
| { | ||
| options.ResourceMetadata = new() | ||
| { | ||
| Resource = new Uri(serverUrl), | ||
| ResourceDocumentation = new Uri("https://docs.example.com/api/weather"), | ||
| AuthorizationServers = { new Uri(azureB2CAuthority) }, | ||
| ScopesSupported = ["mcp:tools"], | ||
| }; | ||
| }); | ||
|
|
||
| builder.Services.AddAuthorization(); | ||
|
|
||
| builder.Services.AddHttpContextAccessor(); | ||
| builder.Services.AddMcpServer() | ||
| .WithTools<WeatherTools>() | ||
| .WithHttpTransport(); | ||
|
|
||
| // Configure HttpClientFactory for weather.gov API | ||
| builder.Services.AddHttpClient("WeatherApi", client => | ||
| { | ||
| client.BaseAddress = new Uri("https://api.weather.gov"); | ||
| client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); | ||
| }); | ||
|
|
||
| var app = builder.Build(); | ||
|
|
||
| app.UseAuthentication(); | ||
| app.UseAuthorization(); | ||
|
|
||
| // Use the default MCP policy name that we've configured | ||
| app.MapMcp().RequireAuthorization(); | ||
|
|
||
| Console.WriteLine($"Starting Azure B2C Client Credentials MCP server with authorization at {serverUrl}"); | ||
| Console.WriteLine($"Using Azure B2C authority: {azureB2CAuthority}"); | ||
| Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource"); | ||
| Console.WriteLine("Press Ctrl+C to stop the server"); | ||
|
|
||
| app.Run(serverUrl); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's too early to demo using Azure B2C as an MCP OAuth provider since it does not support RFC 8707 which is required by the MCP spec. That's why this sample is unable to validate that the token was issued specifically for the resource URI specified in the ProtectedResourceMetadata like we do in the ProtectedMCPServer sample.
csharp-sdk/samples/ProtectedMCPServer/Program.cs
Line 28 in 1232456
Here are the relevant parts of the MCP authorization specification that mandate that the MCP server MUST validate that the access token was intended specifically for them as the intended audience
https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-handling
https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-audience-binding-and-validation
https://den.dev/blog/mcp-authorization-resource/ written by @localden provides a more in-depth explanation for why RFC 8707 is important. The tl;dr is that without the appropriate audience validation an attacker could host an MCP server with the same
azureB2CClientIdas your server, but with a different resource URI and trick victims to log into it. The attacker could then pass off victims' access tokens to your MCP server as their own and do whatever they want with it, and you'd have no way of knowing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, very informative to what we were trying to do. Since B2C probably isn't gonna update anytime soon to support this, we will have to put aside MCP replacing our semantic kernel plugins for now unless we make some other changes to our auth pipeline which isn't likely atm. I'll cancel the PR.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MCP does not properly implement RFC 8707.
RFC 8707 explicitly states that "a client MAY indicate the protected resource... by including the
resourceparameter" (RFC 8707 Section 2-1). MCP therefore turns a "MAY" into a "MUST", which is a deviation. Please see this discussion for more details: modelcontextprotocol/modelcontextprotocol#1599Entra uses the
scopeparameter as per OIDC guidelines.IMO, at less than a year old, MCP should strive to support major IdP's rather than strive for major IdP's to support MCP.