使用OAuth DPoP和Duende身份提供程序构建安全的MCP服务器

本文详细介绍了如何使用ASP.NET Core构建安全的MCP服务器,集成OAuth DPoP和Duende身份提供程序。涵盖身份验证流程、DPoP令牌绑定、客户端配置及完整的代码实现,为构建企业级安全应用提供实践指导。

使用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身份验证)进行操作。

链接

标准、草案标准

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计