Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj
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>
113 changes: 113 additions & 0 deletions samples/AzureB2CClientCredentials/Program.cs
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 },
Copy link
Contributor

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.

ValidAudience = serverUrl, // Validate that the audience matches the resource metadata as suggested in RFC 8707

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

Token Handling

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. If validation fails, servers MUST respond according to OAuth 2.1 Section 5.3 error handling requirements. Invalid or expired tokens MUST receive a HTTP 401 response.

https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-handling

Token Audience Binding and Validation

RFC 8707 Resource Indicators provide critical security benefits by binding tokens to their intended audiences when the Authorization Server supports the capability. To enable current and future adoption:

  • MCP clients MUST include the resource parameter in authorization and token requests as specified in the [Resource Parameter Implementation (#resource-parameter-implementation) section
  • MCP servers MUST validate that tokens presented to them were specifically issued for their use

The Security Best Practices document outlines why token audience validation is crucial and why token passthrough is explicitly forbidden.

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 azureB2CClientId as 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.

Copy link
Author

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.

Copy link

@arcaputo3 arcaputo3 Oct 2, 2025

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 resource parameter" (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#1599

Entra uses the scope parameter 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.

// 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);
Loading