-
Notifications
You must be signed in to change notification settings - Fork 721
Description
Describe the bug
The OAuth authorization url does not contain the state
parameter, which despite being recommended in the OAuth 2.1 specification, is mandatory for Okta who cite the OAuth 2.0 Threat Model and Security Considerations as reasons for making it mandatory.
Example url generated by inspector (note the lack of state
parameter):
https://mycompany.okta.com/oauth2/auscvfmyredacted/v1/authorize?response_type=code&client_id=0oa17oksredacted&code_challenge=3RvqeV5qmeAk-7sT1a4b7tik6BPGe-redacted&code_challenge_method=S256&redirect_uri=http://localhost:6274/oauth/callback&scope=openid&resource=http://localhost:7071/
Okta being a mega-provider of identity services, for the MCP to gather more adoption it and Inspector should accommodate companies, like mine, that use Okta (and also meet security best practice).
To Reproduce
Steps to reproduce the behavior:
- In an active okta tenant (demo or preview are fine) set up an OIDC (Authorisation Code) Okta application (with the correct redirect uri) and grant it permissions on an Authorization server (issuer).
- Get the ProtectedMcpServer sample from the MCP/C# github repo) and change the program.cs file to the following:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.AspNetCore.Authentication;
using System.Net.Http.Headers;
using System.Security.Claims;
using AspNetCoreSseServer.Tools;
var builder = WebApplication.CreateBuilder(args);
var serverUrl = "http://localhost:7071/";
var inMemoryOAuthServerUrl = "http://localhost:7071";
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure to validate tokens from our in-memory OAuth server
options.Authority = inMemoryOAuthServerUrl;
options.RequireHttpsMetadata = false; // Allow HTTP for local testing ONLY
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = serverUrl,
ValidIssuer = inMemoryOAuthServerUrl,
NameClaimType = "name",
RoleClaimType = "roles"
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var name = context.Principal?.Identity?.Name ?? "unknown";
var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
Console.WriteLine($"Token validated for: {name} ({email})");
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
return Task.CompletedTask;
},
OnChallenge = context =>
{
Console.WriteLine($"Challenging client to authenticate");
return Task.CompletedTask;
}
};
})
.AddMcp(options =>
{
options.ResourceMetadata = new()
{
Resource = new Uri(serverUrl),
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
AuthorizationServers = { new Uri(inMemoryOAuthServerUrl) },
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();
// Add OAuth authorization server discovery endpoint
app.MapGet("/.well-known/oauth-authorization-server", async (IHttpClientFactory httpClientFactory) =>
{
try
{
using var httpClient = httpClientFactory.CreateClient();
var response = await httpClient.GetAsync("https://mycompany.okta.com/oauth2/auscvredacted/.well-known/oauth-authorization-server");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/json";
return Results.Content(content, contentType);
}
else
{
return Results.StatusCode((int)response.StatusCode);
}
}
catch (Exception ex)
{
return Results.Problem($"Error proxying OAuth discovery document: {ex.Message}");
}
});
// Use the default MCP policy name that we've configured
app.MapMcp().RequireAuthorization();
Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
Console.WriteLine($"Using in-memory OAuth server at {inMemoryOAuthServerUrl}");
Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
Console.WriteLine("Press Ctrl+C to stop the server");
app.Run(serverUrl);
- Connect to it through Inspector, by provding the ClientID of the Okta application.
- In developer tools observe the URL that was sent to the client - and note that Okta rejects the call to /authorize because the
state
parameter is missing. Also see the response from Okta via thelocation
header of the 302 response.
Expected behavior
In the call to the /authorize
endpoint (and in all OAuth interactions where available in the spec) the state
parameter should be included in the url as a parameter. This is consistent with security best practice.
Corresponding header in the response from Okta:
location
= http://localhost:6274/oauth/callback?error=invalid_request&error_description=The+authentication+request+has+an+invalid+%27state%27+parameter
Additional context
This was observed when attempting to build out a remote MCP server, running locally (built in C#, using the official MCP SDK).