使用OAuth DPoP和Duende身份提供程序实现安全的MCP服务器
本文演示了ASP.NET Core应用程序如何通过OpenID Connect和OAuth连接安全的MCP服务器。两个应用程序都使用Duende IdentityServer作为身份提供程序。MCP服务器需要委托的DPoP访问令牌。
代码库:https://github.com/damienbod/McpOidcOAuth
设置
UI应用程序使用OpenID Connect与Duende IdentityServer进行身份验证。成功验证后,会颁发DPoP访问令牌,并用于访问模型上下文协议(MCP)服务器。
DPoP(持有证明演示)无需相互TLS(MTLS)即可实现令牌绑定,通过使令牌盗窃显著更困难来增强API安全性。要使用令牌,客户端必须拥有与之关联的私钥。
使用OAuth DPoP的MCP服务器
MCP服务器需要由Duende IdentityServer颁发的访问令牌。这些JWT令牌使用标准机制以及额外的DPoP验证进行验证。DPoP验证通过Duende.AspNetCore.Authentication.JwtBearer NuGet包实现。所有MCP服务器端点都需要有效的DPoP访问令牌才能访问。
所需包:
- ModelContextProtocol.AspNetCore
- Microsoft.AspNetCore.Authentication.JwtBearer
- Duende.AspNetCore.Authentication.JwtBearer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
var httpMcpServerUrl = builder.Configuration["HttpMcpServerUrl"];
var identityProvider = builder.Configuration["IdentityProvider"];
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = identityProvider;
options.Audience = $"{identityProvider}/resources";
options.TokenValidationParameters.ValidateAudience = true;
options.MapInboundClaims = false;
options.TokenValidationParameters.ValidTypes = ["at+jwt"];
})
.AddMcp(options =>
{
options.ResourceMetadata = new()
{
Resource = new Uri($"{httpMcpServerUrl}/mcp"),
ResourceName = "MCP demo server",
AuthorizationServers = [ new Uri(identityProvider!) ],
DpopBoundAccessTokensRequired = true,
ResourceDocumentation = new Uri($"{httpMcpServerUrl}/health"),
ScopesSupported = ["mcp:tools"],
};
});
// 将DPoP层叠到上面的"token"方案
builder.Services.ConfigureDPoPTokensForScheme("Bearer", opt =>
{
});
builder.Services.AddAuthorization();
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithPrompts<PromptExamples>()
.WithResources<DocumentationResource>()
.WithTools<RandomNumberTools>()
.WithTools<DateTools>();
builder.Services.AddHttpClient();
// 如果不使用MS的魔术命名空间,请更改为scp或scope
// 必须验证范围,因为我们希望强制仅使用委托访问令牌
// 需要此范围以仅允许针对此API的访问令牌
builder.Services.AddAuthorizationBuilder()
.AddPolicy("mcp_tools", policy =>
policy.RequireClaim("scope", "mcp:tools"));
// 添加服务到容器
|
还实现了授权以要求"mcp:tools"范围。
1
2
3
4
|
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp("/mcp").RequireAuthorization("mcp_tools");
|
MCP客户端
MCP客户端作为ASP.NET Core Web应用程序的一部分实现。此应用程序需要用户和应用程序本身都经过身份验证。身份验证使用带有PKCE的OpenID Connect代码流执行,应用程序请求DPoP绑定的访问令牌。Duende IdentityServer用作身份提供程序。
所需包:
- Duende.AccessTokenManagement.OpenIdConnect
- Microsoft.AspNetCore.Authentication.OpenIdConnect
- Microsoft.SemanticKernel
- ModelContextProtocol
- ModelContextProtocol.AspNetCore
AddOpenIdConnect方法用于在ASP.NET Core应用程序中配置OpenID Connect客户端。令牌访问管理通过AddOpenIdConnectAccessTokenManagement方法处理,由Duende客户端NuGet包提供。AddUserAccessTokenHttpClient方法注册一个HTTP客户端,该客户端自动在所有传出HTTP请求中包含DPoP访问令牌和DPoP证明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = false;
options.Events.OnSigningOut = async e =>
{
await e.HttpContext.RevokeRefreshTokenAsync();
};
}).AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:5101";
options.ClientId = "McpWebClient";
options.ClientSecret = "ddedF4f289k$3eDa23ed0iTk4Raq&tttk23d08nhzd";
options.ResponseType = "code";
options.ResponseMode = "query";
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("mcp:tools");
options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
var privatePem = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "ecdsa384-private.pem"));
var publicPem = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "ecdsa384-public.pem"));
var ecdsaCertificate = X509Certificate2.CreateFromPem(publicPem, privatePem);
var ecdsaCertificateKey = new ECDsaSecurityKey(ecdsaCertificate.GetECDsaPrivateKey());
// 添加自动令牌管理
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>
{
var jwk = JsonWebKeyConverter.ConvertFromSecurityKey(ecdsaCertificateKey);
jwk.Alg = "ES384";
options.DPoPJsonWebKey = DPoPProofKey.ParseOrDefault(JsonSerializer.Serialize(jwk));
});
builder.Services.AddHttpClient();
builder.Services.AddUserAccessTokenHttpClient("dpop-api-client", configureClient: client =>
{
client.BaseAddress = new Uri("https://localhost:5103");
});
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
|
CreateMcpTransport方法使用包含DPoP访问令牌的HttpClient。
1
2
3
4
5
6
|
private IClientTransport CreateMcpTransport(IHttpClientFactory clientFactory)
{
var httpClient = clientFactory.CreateClient("dpop-api-client");
var httpMcpServerUrl = _configuration["HttpMcpServerUrl"] ?? throw new ArgumentNullException("Configuration missing for HttpMcpServerUrl");
return new SseClientTransport(new() { Endpoint = new Uri(httpMcpServerUrl), Name = "Secure Client" }, httpClient);
}
|
这在EnsureSetupAsync方法中使用,该方法设置MCP客户端。
1
2
3
4
5
6
7
8
9
10
|
public async Task EnsureSetupAsync(IHttpClientFactory clientFactory)
{
if (_initialized) return;
_mcpClient = await McpClientFactory.CreateAsync(CreateMcpTransport(clientFactory), GetMcpOptions());
await _kernel.ImportMcpClientToolsAsync(_mcpClient);
_promptingService = new PromptingService(_kernel, autoInvoke: _mode == ApprovalMode.Elicitation);
_initialized = true;
}
|
Duende IdentityServer客户端配置
为此,Duende IdentityServer需要Web应用程序配置和API的范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public static IEnumerable<ApiScope> ApiScopes =>[
new ApiScope("mcp:tools")];
public static IEnumerable<Client> Clients =>[
new Client
{
ClientId = "McpWebClient",
// 在实际应用中,使用密钥库
ClientSecrets = { new Secret("ddedF4f289k$3eDa23ed0iTk4Raq&tttk23d08nhzd".Sha256()) },
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
RedirectUris = { "https://localhost:5102/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:5102/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:5102/signout-callback-oidc" },
RequireDPoP = true,
RequirePushedAuthorization = true,
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "offline_access", "mcp:tools" }
}];
|
注意事项
为MCP服务器使用OAuth DPoP访问令牌通过令牌绑定增加了额外的安全层。在此企业设置中,无需额外的安全协议来保护MCP服务器。MCP服务器像任何其他API一样运行,客户端代表经过身份验证的身份(应用程序和用户通过使用Duende IdentityServer的OpenID Connect身份验证)进行操作。
链接
标准、草案标准