Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

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 令牌的典型结构

json
{
  "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+jwtResource AS 必须校验,否则有混淆 deputy 风险
iss企业 IdP 的 issuerResource AS 必须维护可信 IdP 白名单
sub用户在企业内的 opaque ID仅对 iss 唯一,真实用户键是 iss+sub
aud目标 Resource AS防止令牌被转发到其他授权服务器
resource要访问的 MCP Serveraccess token 必须限制 audience 为该 server
scope权限上限Resource AS 可授予更小或相等的 scope,不能放大
jti唯一标识短效期内做 replay 保护

3.2 为什么 sub 不能单独作为用户键?

sub 只在 iss 范围内唯一。如果两个不同 IdP 碰巧给不同用户分配了相同的 sub,单独用 sub 做权限判断就会串户。正确的用户键是 iss + sub 的组合。


四、完整协议流程

4.1 步骤 1:发现(Discovery)

Client 首先要确认两件事:

  1. Resource AS 是谁:通过 MCP Server 的 Protected Resource Metadata(RFC 9728)获取。
  2. Resource AS 是否支持 EMA:检查 AS 元数据里的 authorization_grant_profiles_supported 是否包含 urn:ietf:params:oauth:grant-profile:id-jag
json
{
  "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:

json
{
  "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

http
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.json

Resource AS 验证:

  • JWT 签名是否来自可信 IdP。
  • typ 是否为 oauth-id-jag+jwt
  • aud 是否指向自己。
  • resource 是否是自己代理的 MCP Server。
  • client_id 是否与认证客户端一致。
  • exp/iat 是否在允许时间窗口内。
  • jti 是否没有重放。

验证通过后,返回:

json
{
  "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 端点,演示核心校验逻辑(非生产代码):

go
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

go
// 错误:按品牌信任
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 给企业带来的改变

场景传统 MCPEMA
员工入职手动授权 20+ 服务登录一次,自动继承已批准服务
权限变更逐一服务修改IdP 统一调整 group/role
离职逐一撤销授权禁用企业身份,全部失效
审计分散在各服务集中在 IdP 审计日志
合规个人账户混入强制使用企业身份

EMA 把 MCP 从“个人玩具”变成“企业级工具链”的最后一块拼图补上了。


八、总结

MCP 企业托管授权(EMA)是 2026 年 AI Agent 企业落地最重要的基础设施更新之一。它不改变 MCP 的核心协议,而是在 OAuth 2 之上加了一层企业治理

  • ID-JAG 把企业身份断言标准化。
  • 让 IdP 成为访问决策的单一权威。
  • 实现员工一次登录、自动继承权限的零点击体验。
  • 把审计和合规从“不可能”变成“自然发生”。

对于正在部署 Claude Code、Cursor、Copilot 等 AI 工具的企业安全团队来说,EMA 不是可选项,而是必须纳入规划的基线能力。


参考链接

  1. MCP Enterprise-Managed Authorization Official Blog
  2. MCP 中文文档:企业托管授权
  3. Enterprise-Managed Authorization: what it actually does
  4. RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants
  5. RFC 9728: OAuth 2.0 Protected Resource Metadata
  6. Okta Cross App Access Protocol

上次更新于: