保护 AI 工作流程:模型上下文协议服务器的 Entra ID 身份验证
介绍
保护模型上下文协议 (MCP) 服务器的安全与锁好家门一样重要。保持 MCP 服务器开放会导致您的工具和数据遭受未经授权的访问,从而导致安全漏洞。Microsoft Entra ID 提供强大的基于云的身份和访问管理解决方案,帮助确保只有授权用户和应用程序才能与您的 MCP 服务器交互。在本节中,您将学习如何使用 Entra ID 身份验证保护您的 AI 工作流。
学习目标
读完本节后,您将能够:
- 了解保护 MCP 服务器的重要性。
- 解释 Microsoft Entra ID 和 OAuth 2.0 身份验证的基础知识。
- 认识到公共客户和机密客户之间的区别。
- 在本地(公共客户端)和远程(机密客户端)MCP 服务器场景中实现 Entra ID 身份验证。
- 在开发 AI 工作流程时应用安全最佳实践。
安全性和 MCP
正如您不会不锁门一样,您也不应该将您的 MCP 服务器开放给任何人访问。保护您的 AI 工作流程对于构建强大、可靠且安全的应用程序至关重要。本章将向您介绍如何使用 Microsoft Entra ID 保护您的 MCP 服务器,确保只有授权用户和应用程序才能与您的工具和数据交互。
为什么安全对于 MCP 服务器至关重要
假设您的 MCP 服务器上有一个可以发送电子邮件或访问客户数据库的工具。不安全的服务器意味着任何人都有可能使用该工具,从而导致未经授权的数据访问、垃圾邮件或其他恶意活动。
通过实施身份验证,您可以确保对服务器的每个请求都经过验证,从而确认发出请求的用户或应用程序的身份。这是保护 AI 工作流程安全的第一步,也是最关键的一步。
Microsoft Entra ID 简介
Microsoft Entra ID是一项基于云的身份和访问管理服务。您可以将其视为应用程序的通用安全卫士。它负责处理验证用户身份(身份验证)和确定用户权限(授权)的复杂流程。
通过使用 Entra ID,您可以:
- 为用户启用安全登录。
- 保护 API 和服务。
- 从中心位置管理访问策略。
对于 MCP 服务器,Entra ID 提供了一个强大且广受信赖的解决方案来管理谁可以访问您的服务器的功能。
了解魔法:Entra ID 身份验证的工作原理
Entra ID 使用OAuth 2.0等开放标准来处理身份验证。虽然细节可能很复杂,但核心概念很简单,可以用类比来理解。
OAuth 2.0 简介:Valet Key
可以将 OAuth 2.0 想象成您的汽车代客泊车服务。当您到达餐厅时,您无需将主钥匙交给代客泊车员。相反,您需要提供一把权限有限的代客泊车钥匙——它可以启动汽车并锁上车门,但不能打开后备箱或杂物箱。
在这个类比中:
- 您是用户。
- 您的汽车是MCP 服务器,拥有宝贵的工具和数据。
- Valet是Microsoft Entra ID。
- 停车服务员是MCP客户端(尝试访问服务器的应用程序)。
- Valet Key是访问令牌。
访问令牌是您登录后,MCP 客户端从 Entra ID 收到的安全文本字符串。客户端每次请求时都会将此令牌提交给 MCP 服务器。服务器可以验证令牌,以确保请求合法,并且客户端拥有必要的权限,而这一切都无需处理您的实际凭证(例如密码)。
身份验证流程
该过程的实际运作方式如下:
Microsoft 身份验证库 (MSAL) 简介
在深入研究代码之前,有必要介绍一下您将在示例中看到的一个关键组件:Microsoft 身份验证库 (MSAL)。
MSAL 是由 Microsoft 开发的一个库,它使开发人员能够更轻松地处理身份验证。您无需编写所有复杂的代码来处理安全令牌、管理登录和刷新会话,MSAL 会处理这些繁重的工作。
强烈建议使用 MSAL 之类的库,因为:
- 它是安全的:它实施行业标准协议和安全最佳实践,降低了代码中出现漏洞的风险。
- 它简化了开发:它抽象了 OAuth 2.0 和 OpenID Connect 协议的复杂性,允许您仅用几行代码即可为您的应用程序添加强大的身份验证。
- 它是维护的: Microsoft 积极维护和更新 MSAL,以应对新的安全威胁和平台变化。
MSAL 支持多种语言和应用程序框架,包括 .NET、JavaScript/TypeScript、Python、Java、Go 以及 iOS 和 Android 等移动平台。这意味着你可以在整个技术堆栈中使用相同的一致身份验证模式。
要了解有关 MSAL 的更多信息,可以查看官方MSAL 概述文档。
使用 Entra ID 保护您的 MCP 服务器:分步指南
现在,让我们逐步了解如何stdio
使用 Entra ID 保护本地 MCP 服务器(通过 进行通信的服务器)。此示例使用公共客户端,适用于在用户计算机上运行的应用程序,例如桌面应用程序或本地开发服务器。
场景 1:保护本地 MCP 服务器(使用公共客户端)
在这个场景中,我们将介绍一个本地运行的 MCP 服务器,它通过 进行通信stdio
,并使用 Entra ID 对用户进行身份验证,然后才允许访问其工具。该服务器将包含一个工具,用于从 Microsoft Graph API 获取用户的个人资料信息。
1. 在Entra ID中设置应用程序
在编写任何代码之前,您需要在 Microsoft Entra ID 中注册您的应用程序。这会告知 Entra ID 您的应用程序,并授予其使用身份验证服务的权限。
- 导航到Microsoft Entra 门户。
- 转到应用程序注册并单击新注册。
- 为您的应用程序命名(例如“我的本地 MCP 服务器”)。
- 对于支持的帐户类型,请选择仅限此组织目录中的帐户。
- 对于此示例,您可以将重定向 URI留空。
- 单击“注册”。
注册后,请记下应用程序(客户端)ID和目录(租户)ID。您将在代码中需要用到这些信息。
2. 代码:解析
让我们看一下处理身份验证的关键代码部分。此示例的完整代码可在mcp-auth-servers GitHub 仓库的Entra ID - Local - WAM文件夹中找到。
AuthenticationService.cs
该类负责处理与Entra ID的交互。
CreateAsync
:此方法从 MSAL(Microsoft 身份验证库)初始化PublicClientApplication
。它使用您的应用程序的clientId
和进行配置tenantId
。WithBroker
:这使得可以使用代理(如 Windows Web 帐户管理器),从而提供更安全、更无缝的单点登录体验。AcquireTokenAsync
:这是核心方法。它首先尝试以静默方式获取令牌(这意味着如果用户已经拥有有效会话,则无需再次登录)。如果无法获取静默令牌,它将提示用户以交互方式登录。
// Simplified for clarity
public static async Task<AuthenticationService> CreateAsync(ILogger<AuthenticationService> logger)
{var msalClient = PublicClientApplicationBuilder.Create(_clientId) // Your Application (client) ID.WithAuthority(AadAuthorityAudience.AzureAdMyOrg).WithTenantId(_tenantId) // Your Directory (tenant) ID.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows)).Build();// ... cache registration ...return new AuthenticationService(logger, msalClient);
}public async Task<string> AcquireTokenAsync()
{try{// Try silent authentication firstvar accounts = await _msalClient.GetAccountsAsync();var account = accounts.FirstOrDefault();AuthenticationResult? result = null;if (account != null){result = await _msalClient.AcquireTokenSilent(_scopes, account).ExecuteAsync();}else{// If no account, or silent fails, go interactiveresult = await _msalClient.AcquireTokenInteractive(_scopes).ExecuteAsync();}return result.AccessToken;}catch (Exception ex){_logger.LogError(ex, "An error occurred while acquiring the token.");throw; // Optionally rethrow the exception for higher-level handling}
}
Program.cs
这是设置 MCP 服务器并集成身份验证服务的地方。
AddSingleton<AuthenticationService>
:这会向依赖注入容器注册AuthenticationService
,因此应用程序的其他部分(如我们的工具)可以使用它。GetUserDetailsFromGraph
工具:此工具需要一个 实例AuthenticationService
。在执行任何操作之前,它会调用authService.AcquireTokenAsync()
来获取有效的访问令牌。如果身份验证成功,它会使用该令牌调用 Microsoft Graph API 并获取用户的详细信息。
// Simplified for clarity
[McpServerTool(Name = "GetUserDetailsFromGraph")]
public static async Task<string> GetUserDetailsFromGraph(AuthenticationService authService)
{try{// This will trigger the authentication flowvar accessToken = await authService.AcquireTokenAsync();// Use the token to create a GraphServiceClientvar graphClient = new GraphServiceClient(new BaseBearerTokenAuthenticationProvider(new TokenProvider(authService)));var user = await graphClient.Me.GetAsync();return System.Text.Json.JsonSerializer.Serialize(user);}catch (Exception ex){return $"Error: {ex.Message}";}
}
3. 一切如何协同工作
- 当 MCP 客户端尝试使用该
GetUserDetailsFromGraph
工具时,该工具首先调用AcquireTokenAsync
。 AcquireTokenAsync
触发 MSAL 库检查有效令牌。- 如果没有找到令牌,MSAL 将通过代理提示用户使用他们的 Entra ID 帐户登录。
- 一旦用户登录,Entra ID 就会发出访问令牌。
- 该工具接收令牌并使用它来安全调用 Microsoft Graph API。
- 用户的详细信息返回给 MCP 客户端。
此过程可确保只有经过身份验证的用户才能使用该工具,从而有效地保护您的本地 MCP 服务器。
场景 2:保护远程 MCP 服务器(使用机密客户端)
当您的 MCP 服务器运行在远程计算机(例如云服务器)上并通过 HTTP Streaming 等协议进行通信时,安全要求会有所不同。在这种情况下,您应该使用保密客户端和授权码流程。这是一种更安全的方法,因为应用程序的机密信息永远不会暴露给浏览器。
此示例使用基于 TypeScript 的 MCP 服务器,该服务器使用 Express.js 处理 HTTP 请求。
1. 在Entra ID中设置应用程序
Entra ID 中的设置与公共客户端类似,但有一个主要区别:您需要创建客户端密钥。
- 导航到Microsoft Entra 门户。
- 在您的应用程序注册中,转到“证书和机密”选项卡。
- 单击“新建客户端密钥”,为其添加描述,然后单击“添加”。
- 重要提示:请立即复制密钥值。您将无法再次看到它。
- 您还需要配置重定向 URI。转到“身份验证”选项卡,点击“添加平台”,选择“Web”,然后输入应用程序的重定向 URI(例如
http://localhost:3001/auth/callback
)。
⚠️重要安全说明:对于生产应用程序,Microsoft 强烈建议使用无密钥身份验证方法(例如托管标识或工作负载联合身份验证),而不是客户端密钥。客户端密钥存在安全风险,因为它们可能被泄露或泄露。托管标识无需在代码或配置中存储凭据,从而提供了一种更安全的方法。
有关托管标识及其实现方式的详细信息,请参阅Azure 资源托管标识概述。
2. 代码:解析
此示例使用基于会话的方法。当用户进行身份验证时,服务器会将访问令牌和刷新令牌存储在会话中,并向用户提供会话令牌。此会话令牌随后将用于后续请求。此示例的完整代码可在mcp-auth-servers GitHub 仓库的“Entra ID - Confidential”客户端文件夹中找到。
Server.ts
该文件设置 Express 服务器和 MCP 传输层。
requireBearerAuth
:这是保护/sse
和端点的中间件。它会检查请求标头/message
中是否存在有效的持有者令牌。Authorization
EntraIdServerAuthProvider
:这是一个实现该McpServerAuthorizationProvider
接口的自定义类。它负责处理 OAuth 2.0 流程。/auth/callback
:此端点负责在用户身份验证后处理来自 Entra ID 的重定向。它将授权码交换为访问令牌和刷新令牌。
// Simplified for clarity
const app = express();
const { server } = createServer();
const provider = new EntraIdServerAuthProvider();// Protect the SSE endpoint
app.get("/sse", requireBearerAuth({provider,requiredScopes: ["User.Read"]
}), async (req, res) => {// ... connect to the transport ...
});// Protect the message endpoint
app.post("/message", requireBearerAuth({provider,requiredScopes: ["User.Read"]
}), async (req, res) => {// ... handle the message ...
});// Handle the OAuth 2.0 callback
app.get("/auth/callback", (req, res) => {provider.handleCallback(req.query.code, req.query.state).then(result => {// ... handle success or failure ...});
});
Tools.ts
该文件定义了 MCP 服务器提供的工具。该getUserDetails
工具与上例中的工具类似,但它从会话中获取访问令牌。
// Simplified for clarity
server.setRequestHandler(CallToolRequestSchema, async (request) => {const { name } = request.params;const context = request.params?.context as { token?: string } | undefined;const sessionToken = context?.token;if (name === ToolName.GET_USER_DETAILS) {if (!sessionToken) {throw new AuthenticationError("Authentication token is missing or invalid. Ensure the token is provided in the request context.");}// Get the Entra ID token from the session storeconst tokenData = tokenStore.getToken(sessionToken);const entraIdToken = tokenData.accessToken;const graphClient = Client.init({authProvider: (done) => {done(null, entraIdToken);}});const user = await graphClient.api('/me').get();// ... return user details ...}
});
auth/EntraIdServerAuthProvider.ts
此类处理以下逻辑:
- 将用户重定向到 Entra ID 登录页面。
- 将授权码交换为访问令牌。
- 将令牌存储在中
tokenStore
。 - 当访问令牌过期时刷新它。
3. 一切如何协同工作
- 当用户首次尝试连接到 MCP 服务器时,
requireBearerAuth
中间件将发现他们没有有效会话,并将他们重定向到 Entra ID 登录页面。 - 用户使用其 Entra ID 帐户登录。
/auth/callback
Entra ID使用授权码将用户重定向回端点。- 服务器用该代码交换访问令牌和刷新令牌,存储它们,并创建发送给客户端的会话令牌。
- 客户端现在可以在
Authorization
标头中使用此会话令牌来对 MCP 服务器发出所有未来请求。 - 当调用该
getUserDetails
工具时,它使用会话令牌查找 Entra ID 访问令牌,然后使用该令牌调用 Microsoft Graph API。
此流程比公共客户端流程更复杂,但对于面向互联网的端点而言是必需的。由于远程 MCP 服务器可通过公共互联网访问,因此需要更强大的安全措施来防范未经授权的访问和潜在的攻击。
安全最佳实践
- 始终使用 HTTPS:加密客户端和服务器之间的通信以保护令牌不被拦截。
- 实施基于角色的访问控制 (RBAC):不仅要检查用户是否已通过身份验证,还要检查他们被授权执行哪些操作。您可以在 Entra ID 中定义角色,并在 MCP 服务器中检查这些角色。
- 监控和审计:记录所有身份验证事件,以便您可以检测并应对可疑活动。
- 处理速率限制和节流:Microsoft Graph 和其他 API 实施速率限制以防止滥用。在 MCP 服务器中实施指数退避和重试逻辑,以妥善处理 HTTP 429(请求过多)响应。考虑缓存频繁访问的数据以减少 API 调用。
- 安全令牌存储: 安全地存储访问令牌和刷新令牌。对于本地应用程序,请使用系统的安全存储机制。对于服务器应用程序,请考虑使用加密存储或安全密钥管理服务,例如 Azure Key Vault。
- 令牌过期处理:访问令牌的有效期有限。使用刷新令牌实现自动令牌刷新,无需重新身份验证即可保持无缝的用户体验。
- 考虑使用 Azure API 管理:虽然直接在 MCP 服务器中实现安全机制可以实现更细粒度的控制,但像 Azure API 管理这样的 API 网关可以自动处理许多安全问题,包括身份验证、授权、速率限制和监控。它们在客户端和 MCP 服务器之间提供了一个集中式安全层。有关将 API 网关与 MCP 结合使用的详细信息,请参阅Azure API 管理:适用于 MCP 服务器的身份验证网关。
关键要点
- 保护您的 MCP 服务器对于保护您的数据和工具至关重要。
- Microsoft Entra ID 为身份验证和授权提供了强大且可扩展的解决方案。
- 对本地应用程序使用公共客户端,对远程服务器使用机密客户端。
- 授权码流程是Web 应用程序最安全的选择。
锻炼
- 想想你可能会构建一个 MCP 服务器。它是本地服务器还是远程服务器?
- 根据您的回答,您会使用公共客户端还是机密客户端?
- 您的 MCP 服务器需要什么权限才能对 Microsoft Graph 执行操作?
实践练习
练习 1:在 Entra ID 中注册应用程序
导航到 Microsoft Entra 门户。为您的 MCP 服务器注册一个新的应用程序。记录应用程序(客户端)ID 和目录(租户)ID。
练习 2:保护本地 MCP 服务器(公共客户端)
- 按照代码示例集成 MSAL(Microsoft 身份验证库)进行用户身份验证。
- 通过调用从 Microsoft Graph 获取用户详细信息的 MCP 工具来测试身份验证流程。
练习 3:保护远程 MCP 服务器(机密客户端)
- 在 Entra ID 中注册一个机密客户端并创建客户端机密。
- 配置您的 Express.js MCP 服务器以使用授权码流。
- 测试受保护的端点并确认基于令牌的访问。
练习 4:应用安全最佳实践
- 为您的本地或远程服务器启用 HTTPS。
- 在服务器逻辑中实现基于角色的访问控制(RBAC)。
- 添加令牌过期处理和安全令牌存储。
资源
-
MSAL 概述文档
了解 Microsoft 身份验证库 (MSAL) 如何跨平台实现安全令牌获取:
Microsoft Learn 上的 MSAL 概述 -
Azure-Samples/mcp-auth-servers GitHub 存储库
演示身份验证流程的 MCP 服务器参考实现:
GitHub 上的 Azure-Samples/mcp-auth-servers -
Azure 资源托管标识概述
了解如何使用系统或用户分配的托管标识消除机密:
Microsoft Learn 上的托管标识概述 -
Azure API 管理:MCP 服务器的身份验证
网关 深入了解如何使用 APIM 作为 MCP 服务器的安全 OAuth2 网关:
Azure API 管理 MCP 服务器的身份验证网关 -
Microsoft Graph 权限参考
Microsoft Graph 的委派和应用程序权限的完整列表:
Microsoft Graph 权限参考