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

上次更新于: