Skip to content

Redirect to /authorize does not include state parameter (required by Okta) #682

@Geasley

Description

@Geasley

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:

  1. 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).
  2. 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);
  1. Connect to it through Inspector, by provding the ClientID of the Okta application.
  2. 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 the location 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.

Logs
Example url:
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/

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIssues and PRs related to authentication and/or authorizationbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions