使用OAuth DPoP与Duende身份提供者构建安全的MCP服务器

本文详细介绍如何使用ASP.NET Core实现一个安全的MCP服务器,结合Duende IdentityServer身份提供者,并通过OAuth DPoP机制增强API安全性,防止令牌盗用。

本文演示了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
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叠加到上面的"Bearer"方案上
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
60
61
62
63
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
23
24
25
26
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 设计