Support incremental scope consent (SEP-835) (#1084)
This commit is contained in:
parent
828d51e9d4
commit
31f8d20a1a
2 changed files with 782 additions and 394 deletions
|
|
@ -1,9 +1,16 @@
|
|||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.AspNetCore.Authentication;
|
||||
using ModelContextProtocol.Authentication;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
|
||||
|
|
@ -88,7 +95,6 @@ public class AuthTests : OAuthTestBase
|
|||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
Scopes = ["mcp:tools"],
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "Test MCP Client",
|
||||
|
|
@ -139,7 +145,6 @@ public class AuthTests : OAuthTestBase
|
|||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"),
|
||||
Scopes = ["mcp:tools"],
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "Test MCP Client (No CIMD)",
|
||||
|
|
@ -203,6 +208,33 @@ public class AuthTests : OAuthTestBase
|
|||
[Fact]
|
||||
public async Task CanAuthenticate_WithTokenRefresh()
|
||||
{
|
||||
var hasForcedRefresh = false;
|
||||
|
||||
Builder.Services.AddHttpContextAccessor();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.ToolCollection = new();
|
||||
})
|
||||
.AddListToolsFilter(next =>
|
||||
{
|
||||
return async (mcpContext, cancellationToken) =>
|
||||
{
|
||||
if (!hasForcedRefresh)
|
||||
{
|
||||
hasForcedRefresh = true;
|
||||
|
||||
var httpContext = mcpContext.Services!.GetRequiredService<IHttpContextAccessor>().HttpContext!;
|
||||
await httpContext.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
|
||||
await httpContext.Response.CompleteAsync();
|
||||
throw new Exception("This exception will not impact the client because the response has already been completed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return await next(mcpContext, cancellationToken);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
|
|
@ -210,18 +242,18 @@ public class AuthTests : OAuthTestBase
|
|||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "test-refresh-client",
|
||||
ClientSecret = "test-refresh-secret",
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
// The test-refresh-client should get an expired token first,
|
||||
// then automatically refresh it to get a working token
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(TestOAuthServer.HasRefreshedToken);
|
||||
}
|
||||
|
||||
|
|
@ -328,25 +360,396 @@ public class AuthTests : OAuthTestBase
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JwtBearerChallenge_DoesNotIncludeResourceMetadata()
|
||||
public async Task AuthorizationFlow_UsesScopeFromProtectedResourceMetadata()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync(authScheme: JwtBearerDefaults.AuthenticationScheme);
|
||||
|
||||
using var unauthorizedResponse = await HttpClient.GetAsync(McpServerUrl, HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedResponse.StatusCode);
|
||||
|
||||
var headerFound = false;
|
||||
foreach (var header in unauthorizedResponse.Headers.WwwAuthenticate)
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
headerFound = true;
|
||||
Assert.Equal("Bearer", header.Scheme);
|
||||
Assert.True(header.Parameter is null || !header.Parameter.Contains("resource_metadata", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
options.ResourceMetadata!.ScopesSupported = ["mcp:tools", "files:read"];
|
||||
});
|
||||
|
||||
Assert.True(headerFound);
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
using var metadataResponse = await HttpClient.GetAsync(new Uri("/.well-known/oauth-protected-resource", UriKind.Relative), TestContext.Current.CancellationToken);
|
||||
metadataResponse.EnsureSuccessStatusCode();
|
||||
string? requestedScope = null;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("mcp:tools files:read", requestedScope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationFlow_UsesScopeFromChallengeHeader()
|
||||
{
|
||||
var challengeScopes = "challenge:read challenge:write";
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.Use(next =>
|
||||
{
|
||||
return async context =>
|
||||
{
|
||||
await next(context);
|
||||
|
||||
if (context.Response.StatusCode != 401)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.Headers.WWWAuthenticate = $"Bearer resource_metadata=\"{McpServerUrl}/.well-known/oauth-protected-resource\", scope=\"{challengeScopes}\"";
|
||||
};
|
||||
});
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapMcp().RequireAuthorization();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
string? requestedScope = null;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(challengeScopes, requestedScope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationFlow_UsesScopeFromForbiddenHeader()
|
||||
{
|
||||
var adminScopes = "admin:read admin:write";
|
||||
|
||||
Builder.Services.AddHttpContextAccessor();
|
||||
Builder.Services.AddMcpServer()
|
||||
.WithTools([
|
||||
McpServerTool.Create([McpServerTool(Name = "admin-tool")]
|
||||
async (IServiceProvider serviceProvider, ClaimsPrincipal user) =>
|
||||
{
|
||||
if (!user.HasClaim("scope", adminScopes))
|
||||
{
|
||||
var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
httpContext.Response.Headers.WWWAuthenticate = $"Bearer error=\"insufficient_scope\", resource_metadata=\"{McpServerUrl}/.well-known/oauth-protected-resource\", scope=\"{adminScopes}\"";
|
||||
await httpContext.Response.CompleteAsync();
|
||||
|
||||
throw new Exception("This exception will not impact the client because the response has already been completed.");
|
||||
}
|
||||
|
||||
return "Admin tool executed.";
|
||||
}),
|
||||
]);
|
||||
|
||||
string? requestedScope = null;
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("mcp:tools", requestedScope);
|
||||
|
||||
var adminResult = await client.CallToolAsync("admin-tool", cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.Equal("Admin tool executed.", adminResult.Content[0].ToString());
|
||||
|
||||
Assert.Equal(adminScopes, requestedScope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationFails_WhenResourceMetadataPortDiffers()
|
||||
{
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.ResourceMetadata!.Resource = new Uri("http://localhost:5999");
|
||||
});
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAuthenticate_WithAuthorizationServerPathInsertionMetadata()
|
||||
{
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.ResourceMetadata!.AuthorizationServers = [new Uri($"{OAuthServerUrl}/tenant1")];
|
||||
});
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
var requests = TestOAuthServer.MetadataRequests.ToArray();
|
||||
Assert.Contains("/.well-known/oauth-authorization-server/tenant1", requests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAuthenticate_WithAuthorizationServerPathFallbacks()
|
||||
{
|
||||
const string issuerPath = "/subdir/tenant2";
|
||||
TestOAuthServer.DisabledMetadataPaths.Add($"/.well-known/oauth-authorization-server{issuerPath}");
|
||||
TestOAuthServer.DisabledMetadataPaths.Add($"/.well-known/openid-configuration{issuerPath}");
|
||||
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.ResourceMetadata!.AuthorizationServers = [new Uri($"{OAuthServerUrl}{issuerPath}")];
|
||||
});
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
$"/.well-known/oauth-authorization-server{issuerPath}",
|
||||
$"/.well-known/openid-configuration{issuerPath}",
|
||||
$"{issuerPath}/.well-known/openid-configuration",
|
||||
"/.well-known/openid-configuration",
|
||||
],
|
||||
TestOAuthServer.MetadataRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAuthenticate_WithResourceMetadataPathFallbacks()
|
||||
{
|
||||
const string resourcePath = "/mcp";
|
||||
List<string> wellKnownRequests = [];
|
||||
|
||||
Builder.Services.Configure<AuthenticationOptions>(options => options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme);
|
||||
await using var app = Builder.Build();
|
||||
|
||||
var metadata = new ProtectedResourceMetadata
|
||||
{
|
||||
Resource = new Uri($"{McpServerUrl}{resourcePath}"),
|
||||
AuthorizationServers = { new Uri(OAuthServerUrl) },
|
||||
};
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/.well-known/oauth-protected-resource", out var remaining))
|
||||
{
|
||||
wellKnownRequests.Add(context.Request.Path);
|
||||
if (remaining.HasValue)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapMcp(resourcePath).RequireAuthorization();
|
||||
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
var endpoint = new Uri(new Uri(McpServerUrl), resourcePath);
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
$"/.well-known/oauth-protected-resource{resourcePath}",
|
||||
"/.well-known/oauth-protected-resource"
|
||||
],
|
||||
wellKnownRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotAuthenticate_WhenResourceMetadataResourceIsNonRootParentPath()
|
||||
{
|
||||
const string configuredResourcePath = "/mcp";
|
||||
const string requestedResourcePath = "/mcp/tools";
|
||||
|
||||
// Remove resource_metadata from the WWW-Authenticate header, because we should only fall back at all (even to root) when it's missing.
|
||||
//
|
||||
// If the protected resource metadata was retrieved from a URL returned by the protected resource via the WWW-Authenticate resource_metadata parameter,
|
||||
// then the resource value returned MUST be identical to the URL that the client used to make the request to the resource server.
|
||||
// If these values are not identical, the data contained in the response MUST NOT be used.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc9728/#section-3.3
|
||||
//
|
||||
// CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath validates we won't fall back to root in this case.
|
||||
// CanAuthenticate_WithResourceMetadataPathFallbacks validates we will fall back to root when resource_metadata is missing.
|
||||
Builder.Services.Configure<AuthenticationOptions>(options => options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme);
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.ResourceMetadata = new ProtectedResourceMetadata
|
||||
{
|
||||
Resource = new Uri($"{McpServerUrl}{configuredResourcePath}"),
|
||||
AuthorizationServers = { new Uri(OAuthServerUrl) },
|
||||
};
|
||||
});
|
||||
|
||||
await using var app = Builder.Build();
|
||||
|
||||
app.MapMcp(requestedResourcePath).RequireAuthorization();
|
||||
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri($"{McpServerUrl}{requestedResourcePath}"),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(async () =>
|
||||
{
|
||||
await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
});
|
||||
|
||||
Assert.Contains("does not match", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath()
|
||||
{
|
||||
const string requestedResourcePath = "/mcp/tools";
|
||||
|
||||
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.ResourceMetadata = new ProtectedResourceMetadata
|
||||
{
|
||||
Resource = new Uri($"{McpServerUrl}"),
|
||||
AuthorizationServers = { new Uri(OAuthServerUrl) },
|
||||
};
|
||||
});
|
||||
|
||||
await using var app = Builder.Build();
|
||||
|
||||
app.MapMcp(requestedResourcePath).RequireAuthorization();
|
||||
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri($"{McpServerUrl}{requestedResourcePath}"),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(async () =>
|
||||
{
|
||||
await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
});
|
||||
|
||||
Assert.Contains("does not match", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using ModelContextProtocol.AspNetCore.Authentication;
|
|||
using ModelContextProtocol.AspNetCore.Tests.Utils;
|
||||
using ModelContextProtocol.Authentication;
|
||||
using System.Net;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
|
||||
|
||||
|
|
@ -84,7 +83,7 @@ public abstract class OAuthTestBase : KestrelInMemoryTest, IAsyncDisposable
|
|||
|
||||
protected async Task<WebApplication> StartMcpServerAsync(string path = "", string? authScheme = null)
|
||||
{
|
||||
Builder.Services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
Builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.TokenValidationParameters.ValidAudience = $"{McpServerUrl}{path}";
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue