MCP 企业托管授权(EMA)2026 实战:零点击 OAuth 与 ID-JAG 身份断言链
引子:企业部署 MCP 的最大堵点
模型上下文协议(MCP)在 2026 年已经不再是新鲜概念。从文件系统到数据库,从 GitHub 到 Slack,成百上千个 MCP 服务器让 AI Agent 可以调用真实业务系统。但企业安全团队卡壳的地方往往不是技术,而是授权:
- 每个员工都要为每个 MCP 服务器点一遍 OAuth 同意框。
- 安全团队无法统一控制谁能访问哪些工具。
- 员工离职后,访问权限散落在几十个小服务的授权记录里。
- 个人账户和工作账户混在一起,审计都审不清。
2026 年 7 月 13 日,MCP 官方团队把 Enterprise-Managed Authorization(EMA) 扩展提升到稳定状态。Anthropic、Microsoft、Okta 率先支持,Claude、Claude Code、Claude Cowork、Visual Studio Code 已经可用。
这篇文章的目标很直接:把 EMA 的协议流程、ID-JAG 令牌、以及落地时要注意的坑,讲清楚。
一、标准 MCP 授权在企业里的三大痛点
1.1 每用户 × 每服务器的授权地狱
员工 Alice 入职第一天:
├── 连接 Slack MCP Server → 点 OAuth 同意
├── 连接 GitHub MCP Server → 点 OAuth 同意
├── 连接 Jira MCP Server → 点 OAuth 同意
├── 连接 Salesforce MCP Server → 点 OAuth 同意
└── 连接 Figma MCP Server → 点 OAuth 同意
结果是:
• 每次授权都是个人行为,IT 无法统一管控
• 授权记录分散在各服务,无法集中审计
• 离职时要在每个服务上单独撤销1.2 策略无法统一执行
标准 OAuth 的授权决策在用户手里。如果 Alice 是客服,她理论上不应该访问生产数据库的 MCP 服务器。但标准 MCP 只能依赖她“自觉不点同意”,安全团队没有抓手。
1.3 工作账户与个人账户混淆
没有企业身份绑定,员工可能用自己的 Gmail Slack 账户完成授权,把工作数据同步到个人工作区。这在合规审计中是大问题。
EMA 解决的就是这三件事:把授权决策从用户手里收回到企业 IdP。
二、EMA 核心架构:IdP 成为权威决策者
┌─────────────────────────────────────────────────────────────┐
│ MCP 企业托管授权架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ 员工 │────────▶│ 企业 IdP │ (Okta / Azure AD) │
│ │ Alice │ SSO │ │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ │ 1. 登录 │ 2. 颁发 ID-JAG │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ MCP Client │ │
│ │ (Claude / VS Code / Cursor) │ │
│ └──────────────┬─────────────────────┘ │
│ │ 3. 用 ID-JAG 换 access token │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ MCP Server Authorization Server │ │
│ │ (Resource AS) │ │
│ └──────────────┬─────────────────────┘ │
│ │ 4. 返回 audience-restricted token │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ MCP Server │ │
│ │ (工具/数据提供方) │ │
│ └────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘关键角色:
- Enterprise IdP:企业身份提供商,负责 SSO 和访问策略。
- MCP Client:运行在企业终端上的 AI 客户端(Claude Desktop、VS Code 等)。
- Resource Authorization Server:MCP 服务器的授权服务器,负责把 ID-JAG 换成 access token。
- MCP Server:最终提供工具能力的服务端。
三、ID-JAG:身份断言 JWT 授权授予
EMA 的核心机制是一种名为 ID-JAG(Identity Assertion JWT Authorization Grant) 的 OAuth 扩展。它目前已经是 IETF 草案,Okta 把它的品牌化版本叫做 Cross App Access。
3.1 ID-JAG 令牌的典型结构
{
"typ": "oauth-id-jag+jwt",
"alg": "RS256",
"kid": "idp-key-2026-07"
}
.
{
"jti": "9e43f81b64a33f20116179",
"iss": "https://acme.okta.com",
"sub": "U019488227",
"email": "alice@acme.com",
"aud": "https://auth.slack.example",
"resource": "https://mcp.slack.example",
"client_id": "f53f191f9311af35",
"scope": "chat.read chat.history",
"iat": 1311280970,
"exp": 1311281970
}关键字段说明:
| 字段 | 含义 | 安全注意 |
|---|---|---|
typ | 必须是 oauth-id-jag+jwt | Resource AS 必须校验,否则有混淆 deputy 风险 |
iss | 企业 IdP 的 issuer | Resource AS 必须维护可信 IdP 白名单 |
sub | 用户在企业内的 opaque ID | 仅对 iss 唯一,真实用户键是 iss+sub |
aud | 目标 Resource AS | 防止令牌被转发到其他授权服务器 |
resource | 要访问的 MCP Server | access token 必须限制 audience 为该 server |
scope | 权限上限 | Resource AS 可授予更小或相等的 scope,不能放大 |
jti | 唯一标识 | 短效期内做 replay 保护 |
3.2 为什么 sub 不能单独作为用户键?
sub 只在 iss 范围内唯一。如果两个不同 IdP 碰巧给不同用户分配了相同的 sub,单独用 sub 做权限判断就会串户。正确的用户键是 iss + sub 的组合。
四、完整协议流程
4.1 步骤 1:发现(Discovery)
Client 首先要确认两件事:
- Resource AS 是谁:通过 MCP Server 的 Protected Resource Metadata(RFC 9728)获取。
- Resource AS 是否支持 EMA:检查 AS 元数据里的
authorization_grant_profiles_supported是否包含urn:ietf:params:oauth:grant-profile:id-jag。
{
"authorization_server": "https://auth.slack.example",
"authorization_grant_profiles_supported": [
"urn:ietf:params:oauth:grant-profile:id-jag"
]
}4.2 步骤 2:Client 声明支持 EMA
在 MCP initialize 请求中,Client 声明自己支持 EMA:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"extensions": {
"io.modelcontextprotocol/enterprise-managed-authorization": {}
}
}
}
}4.3 步骤 3:IdP 颁发 ID-JAG
员工用企业 SSO 登录一次,IdP 根据组织策略决定是否放行某个 MCP Server。如果放行,IdP 颁发 ID-JAG 给 Client。
4.4 步骤 4:Client 用 ID-JAG 换 access token
POST /oauth2/token HTTP/1.1
Host: auth.slack.example
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
&client_id=https://client.example.com/client.jsonResource AS 验证:
- JWT 签名是否来自可信 IdP。
typ是否为oauth-id-jag+jwt。aud是否指向自己。resource是否是自己代理的 MCP Server。client_id是否与认证客户端一致。exp/iat是否在允许时间窗口内。jti是否没有重放。
验证通过后,返回:
{
"token_type": "Bearer",
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"expires_in": 86400,
"scope": "chat.read chat.history"
}注意:这个 access token 的 audience 必须限制在 resource 指定的 MCP Server,不能用于其他服务器。
五、Go 代码示例:模拟 Resource AS 的验证逻辑
下面是一个简化的 Resource AS 端点,演示核心校验逻辑(非生产代码):
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// IDJAGClaims 定义 ID-JAG 的 payload
type IDJAGClaims struct {
jwt.RegisteredClaims
Resource string `json:"resource"`
Scope string `json:"scope"`
Email string `json:"email,omitempty"`
}
func validateIDJAG(tokenString string, trustedIssuer string, expectedAudience string, resourceAS *ecdsa.PublicKey) (*IDJAGClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &IDJAGClaims{}, func(token *jwt.Token) (interface{}, error) {
// 必须验证 typ 头
if token.Header["typ"] != "oauth-id-jag+jwt" {
return nil, fmt.Errorf("invalid typ header: %v", token.Header["typ"])
}
return resourceAS, nil
}, jwt.WithIssuer(trustedIssuer), jwt.WithAudience(expectedAudience))
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*IDJAGClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid claims")
}
// 检查 resource 是否属于本 AS 代理的 MCP Server
if !strings.HasPrefix(claims.Resource, "https://mcp.") {
return nil, fmt.Errorf("untrusted resource: %s", claims.Resource)
}
return claims, nil
}
func main() {
// 生成测试密钥
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
publicKey := &privateKey.PublicKey
// 构造 ID-JAG
now := time.Now()
claims := IDJAGClaims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "https://acme.okta.com",
Subject: "U019488227",
Audience: jwt.ClaimStrings{"https://auth.slack.example"},
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)),
IssuedAt: jwt.NewNumericDate(now),
ID: "9e43f81b64a33f20116179",
},
Resource: "https://mcp.slack.example",
Scope: "chat.read chat.history",
Email: "alice@acme.com",
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["typ"] = "oauth-id-jag+jwt"
tokenString, err := token.SignedString(privateKey)
if err != nil {
panic(err)
}
fmt.Println("Generated ID-JAG:")
fmt.Println(tokenString)
// 验证
validated, err := validateIDJAG(tokenString, "https://acme.okta.com", "https://auth.slack.example", publicKey)
if err != nil {
panic(err)
}
fmt.Println("\nValidated claims:")
fmt.Printf(" Issuer: %s\n", validated.Issuer)
fmt.Printf(" Subject: %s\n", validated.Subject)
fmt.Printf(" Resource: %s\n", validated.Resource)
fmt.Printf(" Scope: %s\n", validated.Scope)
}六、落地时的关键安全决策
6.1 多租户 IdP 信任
企业 SaaS 的 Resource AS 通常是多租户的。你不能信任“Okta”这个品牌,只能信任某个具体 tenant 的 issuer URL。
// 错误:按品牌信任
trustedIdPs := []string{"okta.com"}
// 正确:按 issuer URL 信任,按 tenant 隔离
trustedIdPs := map[string][]string{
"acme": {"https://acme.okta.com"},
"contoso": {"https://contoso.auth0.com"},
}6.2 Scope 是上限,不是下限
ID-JAG 里的 scope 只是请求权限。Resource AS 应该根据企业策略、用户角色、资源敏感度,授予相等或更小的 scope。绝不能放大权限。
6.3 短期令牌与重放保护
ID-JAG 的生命周期通常只有 5 分钟,且只能兑换一次。Resource AS 必须在短暂窗口内对 jti 做重放保护。短期 + 一次性,让窃取令牌后的利用窗口极小。
6.4 Audience 限制
Access token 必须绑定到具体 MCP Server。如果拿到 Slack MCP Server 的 token 也能访问 GitHub MCP Server,整个授权模型就崩溃了。
七、EMA 给企业带来的改变
| 场景 | 传统 MCP | EMA |
|---|---|---|
| 员工入职 | 手动授权 20+ 服务 | 登录一次,自动继承已批准服务 |
| 权限变更 | 逐一服务修改 | IdP 统一调整 group/role |
| 离职 | 逐一撤销授权 | 禁用企业身份,全部失效 |
| 审计 | 分散在各服务 | 集中在 IdP 审计日志 |
| 合规 | 个人账户混入 | 强制使用企业身份 |
EMA 把 MCP 从“个人玩具”变成“企业级工具链”的最后一块拼图补上了。
八、总结
MCP 企业托管授权(EMA)是 2026 年 AI Agent 企业落地最重要的基础设施更新之一。它不改变 MCP 的核心协议,而是在 OAuth 2 之上加了一层企业治理:
- 用 ID-JAG 把企业身份断言标准化。
- 让 IdP 成为访问决策的单一权威。
- 实现员工一次登录、自动继承权限的零点击体验。
- 把审计和合规从“不可能”变成“自然发生”。
对于正在部署 Claude Code、Cursor、Copilot 等 AI 工具的企业安全团队来说,EMA 不是可选项,而是必须纳入规划的基线能力。

