From 242b51d58a0b4f6e962b581548930a61df96486f Mon Sep 17 00:00:00 2001 From: vbitzceo Date: Fri, 11 Jul 2025 16:28:00 -0500 Subject: [PATCH 1/2] An example of using Azure B2C as an Oauth provider for an MCP server. Including a testing using a REST client extension to submit json-rpc calls to the sever via the HTTP SSE transport --- .../AzureB2CClientCredentials.csproj | 15 + samples/AzureB2CClientCredentials/Program.cs | 107 +++++++ samples/AzureB2CClientCredentials/README.md | 294 ++++++++++++++++++ .../Tools/HttpClientExt.cs | 13 + .../Tools/WeatherTools.cs | 67 ++++ .../appsettings.json | 15 + .../test-azure-b2c.http | 106 +++++++ 7 files changed, 617 insertions(+) create mode 100644 samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj create mode 100644 samples/AzureB2CClientCredentials/Program.cs create mode 100644 samples/AzureB2CClientCredentials/README.md create mode 100644 samples/AzureB2CClientCredentials/Tools/HttpClientExt.cs create mode 100644 samples/AzureB2CClientCredentials/Tools/WeatherTools.cs create mode 100644 samples/AzureB2CClientCredentials/appsettings.json create mode 100644 samples/AzureB2CClientCredentials/test-azure-b2c.http diff --git a/samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj b/samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj new file mode 100644 index 000000000..b4c35c779 --- /dev/null +++ b/samples/AzureB2CClientCredentials/AzureB2CClientCredentials.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + 783daef3-9c45-408d-a1d3-7caf44724f39 + + + + + + + + \ No newline at end of file diff --git a/samples/AzureB2CClientCredentials/Program.cs b/samples/AzureB2CClientCredentials/Program.cs new file mode 100644 index 000000000..6cc892fde --- /dev/null +++ b/samples/AzureB2CClientCredentials/Program.cs @@ -0,0 +1,107 @@ +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 +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"; +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, // The client ID of your application + 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 => + { + var name = context.Principal?.Identity?.Name ?? "unknown"; + var email = context.Principal?.FindFirstValue("preferred_username") ?? + context.Principal?.FindFirstValue("email") ?? "unknown"; + var objectId = context.Principal?.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier") ?? "unknown"; + Console.WriteLine($"Token validated for: {name} ({email}) - ObjectId: {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() + .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); diff --git a/samples/AzureB2CClientCredentials/README.md b/samples/AzureB2CClientCredentials/README.md new file mode 100644 index 000000000..ecc533378 --- /dev/null +++ b/samples/AzureB2CClientCredentials/README.md @@ -0,0 +1,294 @@ +# Azure B2C Client Credentials MCP Server + +This sample demonstrates how to create an MCP (Model Context Protocol) server that requires OAuth 2.0 Client Credentials authentication using Azure B2C to protect its tools and resources. + +## Overview + +The Azure B2C Client Credentials MCP Server shows how to: +- Create an MCP server with Azure B2C OAuth 2.0 protection +- Configure JWT bearer token authentication with Azure B2C +- Implement protected MCP tools and resources +- Integrate with ASP.NET Core authentication and authorization +- Provide OAuth resource metadata for client discovery + +## Prerequisites + +- .NET 9.0 or later +- Azure B2C tenant +- VSCode with REST Client extension (for testing) + +## Azure B2C Setup + +### 1. Create Azure B2C Tenant + +1. Navigate to the Azure portal +2. Create a new Azure AD B2C tenant +3. Note your tenant name (e.g., `yourtenant.onmicrosoft.com`) + +### 2. Register Application + +1. In your B2C tenant, go to **App registrations** +2. Click **New registration** +3. Configure: + - **Name**: MCP Server API + - **Supported account types**: Accounts in any identity provider or organizational directory + - **Redirect URI**: Leave blank (not needed for client credentials flow) +4. After creation, note the **Application (client) ID** + +### 3. Create User Flow or Custom Policy + +For this client credentials flow example, you need either: + +**Option A: User Flow (Simple)** +1. Go to **User flows** in your B2C tenant +2. Click **New user flow** +3. Select **Sign up and sign in** +4. Choose **Recommended** version +5. Name it `B2C_1_signupsignin` (or your preferred name) + +**Option B: Custom Policy (Advanced)** +1. Create a custom policy (e.g., `B2C_1A_SIGNUP_SIGNIN`) +2. This provides more control over the token issuance + +### 4. Generate Client Secret + +1. Go to **Certificates & secrets** in your app registration +2. Click **New client secret** +3. Add a description and set expiration +4. Copy the secret value (you won't be able to see it again) + +### 5. Configure API Permissions (Optional) + +For client credentials flow, you may want to: +1. Go to **API permissions** in your app registration +2. Add any required permissions for your application +3. Grant admin consent if needed + +## Configuration + +Update the configuration in `appsettings.Development.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Authentication": "Debug" + } + }, + "AzureB2C": { + "Instance": "https://yourtenant.b2clogin.com", + "Tenant": "yourtenant.onmicrosoft.com", + "Policy": "B2C_1_signupsignin", + "ClientId": "your-actual-client-id" + } +} +``` + +**Important Notes:** +- Replace `yourtenant` with your actual B2C tenant name +- Replace `your-actual-client-id` with your application's client ID +- The `Policy` should match the user flow or custom policy you created (e.g., `B2C_1_signupsignin` for user flows or `B2C_1A_SIGNUP_SIGNIN` for custom policies) +- Client secrets are not stored in configuration files for security reasons - they should be provided via environment variables or Azure Key Vault in production + +## Running the Server + +1. Update the Azure B2C configuration in `appsettings.Development.json` +2. Run the server: + ```bash + dotnet run + ``` +3. The server will start at `http://localhost:7071/` + +## What the Server Provides + +### Protected Resources + +- **MCP Endpoint**: `http://localhost:7071/` (requires authentication) +- **OAuth Resource Metadata**: `http://localhost:7071/.well-known/oauth-protected-resource` + +### Available Tools + +The server provides weather-related tools that require authentication: + +1. **get_alerts**: Get weather alerts for a US state + - Parameter: `state` (string) - 2-letter US state abbreviation + - Example: `get_alerts` with `state: "WA"` + +2. **get_forecast**: Get weather forecast for a location + - Parameters: + - `latitude` (number) - Latitude coordinate + - `longitude` (number) - Longitude coordinate + - Example: `get_forecast` with `latitude: 47.6062, longitude: -122.3321` + +> **Note**: Tool names follow the MCP convention of snake_case. The C# method names `GetAlerts` and `GetForecast` are automatically converted to `get_alerts` and `get_forecast` respectively. + +## Testing the Server + +### Using REST Client Extension + +1. Install the REST Client extension in VSCode +2. Use the included `test-azure-b2c.http` file to test the API: + +```http +### Get Azure B2C Token +POST https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1_signupsignin/oauth2/v2.0/token +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials +&client_id=your-client-id +&client_secret=your-client-secret +&scope=https://yourtenant.onmicrosoft.com/your-client-id/.default + +### Test MCP Server Metadata (replace {{token}} with actual token from above) +GET http://localhost:7071/.well-known/oauth-protected-resource +Authorization: Bearer {{token}} + +### Test MCP Tools List +POST http://localhost:7071/ +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "params": {} +} + +### Test Weather Forecast Tool +POST http://localhost:7071/ +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "get_forecast", + "arguments": { + "latitude": 47.6062, + "longitude": -122.3321 + } + } +} +``` + +**Important Notes:** +- Replace `yourtenant` with your actual B2C tenant name +- Replace `your-client-id` with your application's client ID +- Replace `your-client-secret` with your application's client secret +- The scope format is `https://yourtenant.onmicrosoft.com/your-client-id/.default` +- Copy the `access_token` from the first request and use it in subsequent requests + +## Authentication Flow + +1. **Client Credentials Grant**: Client (application) requests token from Azure B2C using client credentials (client ID and client secret) +2. **Token Request**: POST to `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/oauth2/v2.0/token` +3. **Token Response**: Azure B2C returns JWT access token +4. **API Call**: Client includes token in Authorization header: `Bearer ` +5. **Token Validation**: MCP Server validates token against Azure B2C's public keys +6. **Access Granted**: If valid, MCP tools are accessible + +**Key Points:** +- No user interaction required (server-to-server authentication) +- Client must be registered in Azure B2C +- Client secret must be kept secure +- Tokens have expiration times and should be refreshed as needed + +## Architecture + +The server uses: +- **ASP.NET Core** for hosting and HTTP handling +- **JWT Bearer Authentication** for Azure B2C token validation +- **MCP Authentication Extensions** for OAuth resource metadata +- **HttpClient** for calling the weather.gov API +- **Authorization** to protect MCP endpoints + +## Configuration Details + +- **Server URL**: `http://localhost:7071` +- **Azure B2C Authority**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/v2.0` +- **OAuth Metadata**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/v2.0/.well-known/openid-configuration` +- **Token Endpoint**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/oauth2/v2.0/token` +- **Token Validation**: Audience (ClientId) and issuer validation against Azure B2C +- **CORS**: Enabled for all origins (configure appropriately for production) +- **Scope Format**: `https://yourtenant.onmicrosoft.com/your-client-id/.default` + +## Security Considerations + +- Use HTTPS in production +- Implement proper CORS policies +- Use Azure Key Vault for secrets +- Enable logging and monitoring +- Implement rate limiting +- Use least privilege access + +## Troubleshooting + +### Common Issues + +1. **Token validation fails**: + - Check that the Authority URL is correct and matches your B2C tenant and policy + - Verify the policy name matches exactly (case-sensitive) + - Ensure the metadata endpoint is accessible + +2. **Audience validation fails**: + - Ensure ClientId in configuration matches the token audience + - Check that the client ID is correct in both the token request and server configuration + +3. **Issuer validation fails**: + - Verify the Azure B2C policy and tenant configuration + - Check that the issuer URL format matches the expected pattern + +4. **Authentication fails**: + - Verify client secret is correct and not expired + - Check that the scope format is correct: `https://yourtenant.onmicrosoft.com/your-client-id/.default` + - Ensure the token endpoint URL is correct + +5. **CORS issues**: + - Check browser developer tools for CORS-related errors + - Verify CORS policy configuration in the server + +6. **PowerShell Unicode errors**: + - Use the provided test script which handles Unicode properly + - Consider using UTF-8 encoding for HTTP files + +### Debug Logging + +Enable debug logging in `appsettings.Development.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.AspNetCore.Authentication": "Debug" + } + } +} +``` + +## External Dependencies + +- **National Weather Service API**: The weather tools use the free API at `api.weather.gov` to fetch real weather data +- **Microsoft.AspNetCore.Authentication.JwtBearer**: For JWT token validation +- **Microsoft.IdentityModel.Tokens**: For token validation parameters +- **ModelContextProtocol.AspNetCore**: For MCP server functionality and OAuth metadata + +## Key Files + +- `Program.cs`: Server setup with Azure B2C authentication and MCP configuration +- `Tools/WeatherTools.cs`: Weather tool implementations +- `Tools/HttpClientExt.cs`: HTTP client extensions +- `test-azure-b2c.http`: REST client test file +- `appsettings.Development.json`: Development configuration + +## Next Steps + +1. Set up Azure B2C tenant and application +2. Configure the application settings +3. Test the authentication flow +4. Implement additional MCP tools as needed +5. Deploy to Azure with proper security configurations \ No newline at end of file diff --git a/samples/AzureB2CClientCredentials/Tools/HttpClientExt.cs b/samples/AzureB2CClientCredentials/Tools/HttpClientExt.cs new file mode 100644 index 000000000..f7b2b5499 --- /dev/null +++ b/samples/AzureB2CClientCredentials/Tools/HttpClientExt.cs @@ -0,0 +1,13 @@ +using System.Text.Json; + +namespace ModelContextProtocol; + +internal static class HttpClientExt +{ + public static async Task ReadJsonDocumentAsync(this HttpClient client, string requestUri) + { + using var response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + } +} \ No newline at end of file diff --git a/samples/AzureB2CClientCredentials/Tools/WeatherTools.cs b/samples/AzureB2CClientCredentials/Tools/WeatherTools.cs new file mode 100644 index 000000000..b7ab90d14 --- /dev/null +++ b/samples/AzureB2CClientCredentials/Tools/WeatherTools.cs @@ -0,0 +1,67 @@ +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; + +namespace AzureB2CClientCredentials.Tools; + +[McpServerToolType] +public sealed class WeatherTools +{ + private readonly IHttpClientFactory _httpClientFactory; + + public WeatherTools(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + [McpServerTool, Description("Get weather alerts for a US state.")] + public async Task GetAlerts( + [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state) + { + var client = _httpClientFactory.CreateClient("WeatherApi"); + using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}"); + var jsonElement = jsonDocument.RootElement; + var alerts = jsonElement.GetProperty("features").EnumerateArray(); + + if (!alerts.Any()) + { + return "No active alerts for this state."; + } + + return string.Join("\n--\n", alerts.Select(alert => + { + JsonElement properties = alert.GetProperty("properties"); + return $""" + Event: {properties.GetProperty("event").GetString()} + Area: {properties.GetProperty("areaDesc").GetString()} + Severity: {properties.GetProperty("severity").GetString()} + Description: {properties.GetProperty("description").GetString()} + Instruction: {properties.GetProperty("instruction").GetString()} + """; + })); + } + + [McpServerTool, Description("Get weather forecast for a location.")] + public async Task GetForecast( + [Description("Latitude of the location.")] double latitude, + [Description("Longitude of the location.")] double longitude) + { + var client = _httpClientFactory.CreateClient("WeatherApi"); + var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); + using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl); + var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString() + ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); + + using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl); + var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray(); + + return string.Join("\n---\n", periods.Select(period => $""" + {period.GetProperty("name").GetString()} + Temperature: {period.GetProperty("temperature").GetInt32()}°F + Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()} + Forecast: {period.GetProperty("detailedForecast").GetString()} + """)); + } +} diff --git a/samples/AzureB2CClientCredentials/appsettings.json b/samples/AzureB2CClientCredentials/appsettings.json new file mode 100644 index 000000000..9004cee17 --- /dev/null +++ b/samples/AzureB2CClientCredentials/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AzureB2C": { + "Instance": "https://yourtenant.b2clogin.com", + "Tenant": "yourtenant.onmicrosoft.com", + "Policy": "B2C_1_signupsignin", + "ClientId": "your-client-id-here" + } +} diff --git a/samples/AzureB2CClientCredentials/test-azure-b2c.http b/samples/AzureB2CClientCredentials/test-azure-b2c.http new file mode 100644 index 000000000..ce3a8fcb3 --- /dev/null +++ b/samples/AzureB2CClientCredentials/test-azure-b2c.http @@ -0,0 +1,106 @@ +### Azure B2C Client Credentials MCP Server Testing + +# Your Azure B2C configuration +@tenantName = yourtenant +@tenantDomain = yourtenant.onmicrosoft.com +@policyName = B2C_1_signupsignin +@clientId = your-client-id-here +@clientSecret = your-client-secret-here +@serverUrl = http://localhost:7071 + +# This will be populated after running step 1 +@accessToken = your-access-token-here + +# Derived variables +@authority = https://{{tenantName}}.b2clogin.com/{{tenantDomain}}/{{policyName}} +@tokenEndpoint = {{authority}}/oauth2/v2.0/token +@scope = https://{{tenantDomain}}/{{clientId}}/.default + +### 1. Get Access Token from Azure B2C +# Run this first and copy the access_token from the response +# Then update the @accessToken variable above with the token value +POST {{tokenEndpoint}} +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials +&client_id={{clientId}} +&client_secret={{clientSecret}} +&scope={{scope}} + +### 2. Test MCP Server Metadata (make sure to update @accessToken first) +GET {{serverUrl}}/.well-known/oauth-protected-resource +Authorization: Bearer {{accessToken}} + +### 3. Test MCP Server with JSON-RPC (this is the correct way to call MCP tools) +### This is a JSON-RPC request to list available tools +POST {{serverUrl}}/ +Authorization: Bearer {{accessToken}} +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "params": {} +} + +### 4. Test Weather Forecast Tool using JSON-RPC +POST {{serverUrl}}/ +Authorization: Bearer {{accessToken}} +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "get_forecast", + "arguments": { + "latitude": 47.6062, + "longitude": -122.3321 + } + } +} + +### 5. Test Weather Alerts Tool using JSON-RPC +POST {{serverUrl}}/ +Authorization: Bearer {{accessToken}} +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "get_alerts", + "arguments": { + "state": "WA" + } + } +} + +### 6. Test without Authorization (should fail) +POST {{serverUrl}}/ +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/list", + "params": {} +} + +### 7. Test with Invalid Token (should fail) +POST {{serverUrl}}/ +Authorization: Bearer invalid-token +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/list", + "params": {} +} From de208d6d1ea9d39ca1d231008553c008e867204a Mon Sep 17 00:00:00 2001 From: vbitzceo Date: Fri, 11 Jul 2025 17:15:13 -0500 Subject: [PATCH 2/2] removing references to email and username claims, also updated the readme to eplicitly call out Azure B2C's need for an Authority (policy) even for client credentials flow. --- samples/AzureB2CClientCredentials/Program.cs | 20 +++++--- samples/AzureB2CClientCredentials/README.md | 49 +++++++++++-------- .../appsettings.json | 2 +- .../test-azure-b2c.http | 8 +-- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/samples/AzureB2CClientCredentials/Program.cs b/samples/AzureB2CClientCredentials/Program.cs index 6cc892fde..5ac2e70a5 100644 --- a/samples/AzureB2CClientCredentials/Program.cs +++ b/samples/AzureB2CClientCredentials/Program.cs @@ -9,11 +9,14 @@ var serverUrl = "http://localhost:7071/"; -// Azure B2C Configuration +// 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"; @@ -33,7 +36,7 @@ ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, - ValidAudience = azureB2CClientId, // The client ID of your application + ValidAudience = azureB2CClientId, ValidIssuer = azureB2CAuthority, NameClaimType = ClaimTypes.Name, RoleClaimType = ClaimTypes.Role, @@ -47,11 +50,14 @@ { OnTokenValidated = context => { - var name = context.Principal?.Identity?.Name ?? "unknown"; - var email = context.Principal?.FindFirstValue("preferred_username") ?? - context.Principal?.FindFirstValue("email") ?? "unknown"; - var objectId = context.Principal?.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier") ?? "unknown"; - Console.WriteLine($"Token validated for: {name} ({email}) - ObjectId: {objectId}"); + // 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 => diff --git a/samples/AzureB2CClientCredentials/README.md b/samples/AzureB2CClientCredentials/README.md index ecc533378..9fe120ae4 100644 --- a/samples/AzureB2CClientCredentials/README.md +++ b/samples/AzureB2CClientCredentials/README.md @@ -2,6 +2,8 @@ This sample demonstrates how to create an MCP (Model Context Protocol) server that requires OAuth 2.0 Client Credentials authentication using Azure B2C to protect its tools and resources. +> **⚠️ Important Note**: Azure B2C requires a policy/user flow even for client credentials flow, which is different from Azure AD. This is a key architectural difference between Azure B2C (consumer identity) and Azure AD (enterprise identity). + ## Overview The Azure B2C Client Credentials MCP Server shows how to: @@ -35,20 +37,19 @@ The Azure B2C Client Credentials MCP Server shows how to: - **Redirect URI**: Leave blank (not needed for client credentials flow) 4. After creation, note the **Application (client) ID** -### 3. Create User Flow or Custom Policy +### 3. Create User Flow (Required for Azure B2C) -For this client credentials flow example, you need either: +**IMPORTANT**: Unlike Azure AD, Azure B2C requires a policy/user flow even for client credentials flow. This is a key difference between Azure B2C and Azure AD. -**Option A: User Flow (Simple)** +**For Azure B2C Client Credentials Flow:** 1. Go to **User flows** in your B2C tenant 2. Click **New user flow** 3. Select **Sign up and sign in** 4. Choose **Recommended** version 5. Name it `B2C_1_signupsignin` (or your preferred name) +6. Configure the user flow (even though it won't be used for user interaction) -**Option B: Custom Policy (Advanced)** -1. Create a custom policy (e.g., `B2C_1A_SIGNUP_SIGNIN`) -2. This provides more control over the token issuance +**Note**: While the user flow won't be used for actual user sign-in (since this is machine-to-machine authentication), Azure B2C's architecture requires the policy context for token issuance. This is different from Azure AD which supports policy-free client credentials flow. ### 4. Generate Client Secret @@ -80,7 +81,7 @@ Update the configuration in `appsettings.Development.json`: "AzureB2C": { "Instance": "https://yourtenant.b2clogin.com", "Tenant": "yourtenant.onmicrosoft.com", - "Policy": "B2C_1_signupsignin", + "Policy": "B2C_1A_SIGNUP_SIGNIN", "ClientId": "your-actual-client-id" } } @@ -89,7 +90,8 @@ Update the configuration in `appsettings.Development.json`: **Important Notes:** - Replace `yourtenant` with your actual B2C tenant name - Replace `your-actual-client-id` with your application's client ID -- The `Policy` should match the user flow or custom policy you created (e.g., `B2C_1_signupsignin` for user flows or `B2C_1A_SIGNUP_SIGNIN` for custom policies) +- **Policy is Required**: Azure B2C requires the policy even for client credentials flow (unlike Azure AD) +- The `Policy` should match the user flow you created (e.g., `B2C_1_signupsignin`) or custom policy (e.g., `B2C_1A_SIGNUP_SIGNIN`) - Client secrets are not stored in configuration files for security reasons - they should be provided via environment variables or Azure Key Vault in production ## Running the Server @@ -133,7 +135,7 @@ The server provides weather-related tools that require authentication: ```http ### Get Azure B2C Token -POST https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1_signupsignin/oauth2/v2.0/token +POST https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/oauth2/v2.0/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials @@ -186,13 +188,14 @@ Content-Type: application/json ## Authentication Flow 1. **Client Credentials Grant**: Client (application) requests token from Azure B2C using client credentials (client ID and client secret) -2. **Token Request**: POST to `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/oauth2/v2.0/token` -3. **Token Response**: Azure B2C returns JWT access token +2. **Token Request**: POST to `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/oauth2/v2.0/token` +3. **Token Response**: Azure B2C returns JWT access token (note: policy is required in URL even for client credentials) 4. **API Call**: Client includes token in Authorization header: `Bearer ` 5. **Token Validation**: MCP Server validates token against Azure B2C's public keys 6. **Access Granted**: If valid, MCP tools are accessible **Key Points:** +- **Policy Required**: Azure B2C requires a policy in the URL even for client credentials flow (unlike Azure AD) - No user interaction required (server-to-server authentication) - Client must be registered in Azure B2C - Client secret must be kept secure @@ -210,13 +213,19 @@ The server uses: ## Configuration Details - **Server URL**: `http://localhost:7071` -- **Azure B2C Authority**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/v2.0` -- **OAuth Metadata**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/v2.0/.well-known/openid-configuration` -- **Token Endpoint**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policy/oauth2/v2.0/token` +- **Azure B2C Authority**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0` +- **OAuth Metadata**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0/.well-known/openid-configuration` +- **Token Endpoint**: `https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/oauth2/v2.0/token` - **Token Validation**: Audience (ClientId) and issuer validation against Azure B2C - **CORS**: Enabled for all origins (configure appropriately for production) - **Scope Format**: `https://yourtenant.onmicrosoft.com/your-client-id/.default` +**Azure B2C vs Azure AD Differences:** +- **Azure B2C**: Requires policy in URL even for client credentials flow +- **Azure AD**: Supports policy-free client credentials flow +- **Azure B2C**: Designed for consumer identity scenarios +- **Azure AD**: Designed for enterprise/application scenarios + ## Security Considerations - Use HTTPS in production @@ -231,22 +240,22 @@ The server uses: ### Common Issues 1. **Token validation fails**: - - Check that the Authority URL is correct and matches your B2C tenant and policy - - Verify the policy name matches exactly (case-sensitive) - - Ensure the metadata endpoint is accessible + - Check that the Authority URL is correct and matches your B2C tenant + - Verify the metadata endpoint is accessible (no policy needed for client credentials) + - Ensure the tenant domain is correct 2. **Audience validation fails**: - Ensure ClientId in configuration matches the token audience - Check that the client ID is correct in both the token request and server configuration 3. **Issuer validation fails**: - - Verify the Azure B2C policy and tenant configuration - - Check that the issuer URL format matches the expected pattern + - Verify the Azure B2C tenant configuration + - Check that the issuer URL format matches the expected pattern (without policy) 4. **Authentication fails**: - Verify client secret is correct and not expired - Check that the scope format is correct: `https://yourtenant.onmicrosoft.com/your-client-id/.default` - - Ensure the token endpoint URL is correct + - Ensure the token endpoint URL is correct (without policy path) 5. **CORS issues**: - Check browser developer tools for CORS-related errors diff --git a/samples/AzureB2CClientCredentials/appsettings.json b/samples/AzureB2CClientCredentials/appsettings.json index 9004cee17..108cfd0fb 100644 --- a/samples/AzureB2CClientCredentials/appsettings.json +++ b/samples/AzureB2CClientCredentials/appsettings.json @@ -9,7 +9,7 @@ "AzureB2C": { "Instance": "https://yourtenant.b2clogin.com", "Tenant": "yourtenant.onmicrosoft.com", - "Policy": "B2C_1_signupsignin", + "Policy": "B2C_1A_SIGNUP_SIGNIN", "ClientId": "your-client-id-here" } } diff --git a/samples/AzureB2CClientCredentials/test-azure-b2c.http b/samples/AzureB2CClientCredentials/test-azure-b2c.http index ce3a8fcb3..deca20cc3 100644 --- a/samples/AzureB2CClientCredentials/test-azure-b2c.http +++ b/samples/AzureB2CClientCredentials/test-azure-b2c.http @@ -1,9 +1,11 @@ ### Azure B2C Client Credentials MCP Server Testing -# Your Azure B2C configuration +# Your 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 @tenantName = yourtenant @tenantDomain = yourtenant.onmicrosoft.com -@policyName = B2C_1_signupsignin +@policyName = B2C_1A_SIGNUP_SIGNIN @clientId = your-client-id-here @clientSecret = your-client-secret-here @serverUrl = http://localhost:7071 @@ -11,7 +13,7 @@ # This will be populated after running step 1 @accessToken = your-access-token-here -# Derived variables +# Azure B2C endpoints - policy is required even for client credentials @authority = https://{{tenantName}}.b2clogin.com/{{tenantDomain}}/{{policyName}} @tokenEndpoint = {{authority}}/oauth2/v2.0/token @scope = https://{{tenantDomain}}/{{clientId}}/.default