diff --git a/MCPify/Core/McpifyOptions.cs b/MCPify/Core/McpifyOptions.cs index c769539..0b728c0 100644 --- a/MCPify/Core/McpifyOptions.cs +++ b/MCPify/Core/McpifyOptions.cs @@ -64,6 +64,11 @@ public class McpifyOptions /// Defaults to which detects headless environments at runtime. /// public BrowserLaunchBehavior LoginBrowserBehavior { get; set; } = BrowserLaunchBehavior.Auto; + + /// + /// Optional list of OAuth2 configurations to be added to the OAuthConfigurationStore. + /// + public List OAuthConfigurations { get; set; } = new(); } /// diff --git a/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs b/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs index ffd3885..ed5c35c 100644 --- a/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs +++ b/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs @@ -63,7 +63,7 @@ public async Task InvokeAsync(HttpContext context) context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.Headers[HeaderNames.WWWAuthenticate] = - $"Bearer realm=\"MCPify\", resource=\"{resourceUrl}\", resource_metadata_url=\"{metadataUrl}\""; + $"Bearer realm=\"MCPify\", resource=\"{resourceUrl}\", resource_metadata=\"{metadataUrl}\""; return; } diff --git a/MCPify/Hosting/McpifyEndpointExtensions.cs b/MCPify/Hosting/McpifyEndpointExtensions.cs index c01e37f..9837f3b 100644 --- a/MCPify/Hosting/McpifyEndpointExtensions.cs +++ b/MCPify/Hosting/McpifyEndpointExtensions.cs @@ -129,27 +129,29 @@ string BaseUrlProvider() static IEnumerable ResolveAuthorizationServers(OAuth2Configuration config) { - if (config.AuthorizationServers.Count > 0) - { - foreach (var server in config.AuthorizationServers) - { - yield return server; - } - - yield break; - } - if (Uri.TryCreate(config.AuthorizationUrl, UriKind.Absolute, out var uri)) { yield return uri.GetLeftPart(UriPartial.Authority); } } - // Prefer explicitly configured authorization servers, fall back to derived authorities. - var issuers = configs - .SelectMany(ResolveAuthorizationServers) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); + // If any config has AuthorizationServers, only use those; otherwise, fall back to AuthorizationUrl authority. + List issuers; + if (configs.Any(c => c.AuthorizationServers?.Any() == true)) + { + issuers = configs + .Where(c => c.AuthorizationServers?.Any() == true) + .SelectMany(c => c.AuthorizationServers) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + else + { + issuers = configs + .SelectMany(ResolveAuthorizationServers) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } return Results.Ok(new { diff --git a/MCPify/Hosting/McpifyServiceExtensions.cs b/MCPify/Hosting/McpifyServiceExtensions.cs index 23a57c1..b79f0a2 100644 --- a/MCPify/Hosting/McpifyServiceExtensions.cs +++ b/MCPify/Hosting/McpifyServiceExtensions.cs @@ -95,7 +95,13 @@ public static IServiceCollection AddMcpify( }); services.AddSingleton(); - services.AddSingleton(); + + var oauthStore = new OAuthConfigurationStore(); + foreach (var config in opts.OAuthConfigurations) + { + oauthStore.AddConfiguration(config); + } + services.AddSingleton(oauthStore); return services; } diff --git a/Sample/README.md b/Sample/README.md index 3a874f6..bf13419 100644 --- a/Sample/README.md +++ b/Sample/README.md @@ -80,7 +80,7 @@ To run the server in HTTP mode (using Server-Sent Events): This sample demonstrates how clients can authenticate with MCPify using OAuth 2.0 Authorization Code flow. -1. **Discover Authentication**: When an unauthenticated client attempts to use a protected tool (e.g., `api_secrets_get`), MCPify will respond with a `401 Unauthorized` HTTP status code and a `WWW-Authenticate` header, including `resource_metadata_url`. The client should then fetch this metadata. +1. **Discover Authentication**: When an unauthenticated client attempts to use a protected tool (e.g., `api_secrets_get`), MCPify will respond with a `401 Unauthorized` HTTP status code and a `WWW-Authenticate` header, including `resource_metadata`. The client should then fetch this metadata. 2. **Initiate Login**: The client (e.g., Claude Desktop) will call the `login_auth_code_pkce` tool provided by MCPify. This tool returns an authorization URL. 3. **User Authorization**: The user opens the authorization URL in a browser, logs in (using the OpenIddict provider in this sample), and grants consent. 4. **Callback and Token Exchange**: After user authorization, the browser redirects to MCPify's callback endpoint (`/auth/callback`). MCPify handles the code exchange and stores the token securely for the specific session. diff --git a/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs b/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs index 37653ac..04cfe31 100644 --- a/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs +++ b/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs @@ -29,7 +29,7 @@ public async Task Request_Returns401_WhenNoToken_And_OAuthConfigured() Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Contains("WWW-Authenticate", response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)).Keys); var authHeader = response.Headers.WwwAuthenticate.ToString(); - Assert.Contains("resource_metadata_url", authHeader); + Assert.Contains("resource_metadata", authHeader); } [Fact] @@ -53,7 +53,7 @@ public async Task Request_Challenge_UsesResourceOverride() Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); var authHeader = response.Headers.WwwAuthenticate.ToString(); Assert.Contains($"resource=\"{publicUrl}\"", authHeader); - Assert.Contains($"resource_metadata_url=\"{publicUrl}/.well-known/oauth-protected-resource\"", authHeader); + Assert.Contains($"resource_metadata=\"{publicUrl}/.well-known/oauth-protected-resource\"", authHeader); } [Fact]