让 Agent 按用户权限执行任务
教程围绕“哪个 Agent 能代表哪个用户行动”构建授权机制。在 Bedrock AgentCore 上结合 Okta Cross App Access 与 AgentCore Identity,约束任务 Agent 的访问权限。
AI 深度解读
AI Agent 能替用户更新工单、读取客户记录之后,权限问题就不再只是「用户登录了吗」。团队还得回答:哪个 Agent 能替哪个用户访问哪个系统,以及管理员撤销授权后,多久才能真正停下来。
AWS 这篇文章给出的方案,是让 Okta Cross App Access(XAA)负责授权决策,让 Amazon Bedrock AgentCore 执行令牌交换,再由工具服务核验用户权限。作者测到了约 1 秒的新请求拦截速度,也发现了几个会让权限检查失守的配置缺口。它值得读的地方,正是这些边界。
三种令牌,分别回答三个权限问题
共享服务账号容易让 Agent 拿到一份长期通用权限;逐个应用的 OAuth 同意,又难以集中管理「Agent、用户、目标系统」之间的关系。
XAA 把 Agent 注册成身份提供商(IdP)中的一个身份。管理员批准它可以访问哪些应用,Okta 每次收到代用户执行的请求时,再检查 Agent 是否启用、用户是否被分配给它、目标资源和请求范围是否获准。
整条链路有三种主要令牌:
- ID token 证明用户是谁。
- ID-JAG,即 Identity Assertion JWT Authorization Grant,记录 Okta 对本次委托的批准。
- access token 是目标资源接受的访问凭证,同时携带用户和 Agent 身份。
用户先通过 Agent 关联的 OpenID Connect 应用登录。服务于前端的后端(BFF)用用户的 ID token,按 RFC 8693 向 Okta 的组织授权服务器换取 ID-JAG,再把它作为 bearer token 调用 AgentCore runtime。
这里不能随便换一个登录应用。作者遇到过明确拒绝:其他应用签发的 ID token,不能兑换这个 Agent 的委托凭证。
随后,AgentCore Identity 以 Agent 身份,按照 RFC 7523,把 ID-JAG 兑换为资源授权服务器签发的 access token。Agent 再拿它调用两个 Model Context Protocol(MCP)服务。
中间还有一个 AWS 内部的 workload access token,用于绑定工作负载身份与用户。它既不交给 Okta,也不交给目标资源,因此不属于前述三种令牌。
登录成功、委托获批、工具允许执行,是三个不同的判断。
配置最容易错在哪里
示例资源的 audience 是 api://task-manager,自定义 scope 是 xaa:read 和 xaa:write。申请 ID-JAG 时,至少要包含一个自定义 scope,不能混入 openid、profile,否则 Okta 返回 invalid_scope。
资源授权服务器还要允许 Agent 使用 JWT Bearer grant;缺少这条策略,第二次交换会报 no_matching_policy。作者把 access token 有效期设为 5 分钟,并让 groups claim 只包含名称以 xaa- 开头的用户组。
AgentCore Identity 的 credential provider 使用 CustomOauth2,认证方式为 CLIENT_SECRET_BASIC,代用户交换类型设为 JWT_AUTHORIZATION_GRANT。Agent 的密钥由 AWS Secrets Manager 保存,业务代码不必读取密钥或调用 Okta token endpoint。
每个 Okta Agent 都需要自己的 provider。作者拿一个 Agent 的 ID-JAG,以另一个 Agent 身份兑换,Okta 会拒绝,因为签发时的 client_id 必须与兑换时的客户端身份一致。
下面这段代码解决的是:复用 runtime 已经准备好的工作负载令牌,向 Identity 获取资源令牌。
from bedrock_agentcore.runtime import BedrockAgentCoreContext
identity = boto3.client("bedrock-agentcore")
def okta_access_token() -> str:
workload_token = BedrockAgentCoreContext.get_workload_access_token() # 由 runtime 签发
return identity.get_resource_oauth2_token(
workloadIdentityToken=workload_token,
resourceCredentialProviderName="okta-xaa-obo",
scopes=["xaa:read", "xaa:write"], # 必须与 ID-JAG 的 scope 相同
oauth2Flow="ON_BEHALF_OF_TOKEN_EXCHANGE",
)["accessToken"]
要看清 BedrockAgentCoreContext.get_workload_access_token() 这一行:令牌在 SDK 上下文里,不在入口函数收到的请求头里。作者最初找错位置,检查的 21 次请求全部退回了自行调用 GetWorkloadAccessTokenForJWT 的路径,还多用了一个工作负载身份。
入口同样需要收紧。agentcore.json 使用 CUSTOM_JWT,通过 discoveryUrl 指向 Okta 组织授权服务器,allowedAudience 指向资源的自定义授权服务器,allowedScopes 配置允许的范围,allowedClients 则限定当前 Agent 的客户端 ID。
少了 allowedClients,另一个 Agent 的 ID-JAG 可以在入口得到 HTTP 200,到兑换时才失败;加上后,入口直接返回 HTTP 401。
IAM 也不能省。Identity 不会自动绑定同账号下的 workload identity 与 credential provider,必须用角色权限限定两者,并限定 provider 的密钥。相关权限是 bedrock-agentcore:GetResourceOauth2Token 和 secretsmanager:GetSecretValue,provider ARN 的路径段必须写成 oauth2credentialprovider,写成带连字符的形式会触发 AccessDeniedException。
有写入 scope,为什么还不能写入
作者的三个测试用户都拿到了 xaa:read xaa:write,因为资源连接定义的是 Agent 的访问范围。用户之间的权限差异,则放在 groups claim 里:Alice 有 xaa-readers,Bob 有 xaa-writers,Carol 两者都没有。
因此,scope 允许 Agent 调用写工具,不代表每个用户都允许写。
工具端还要确认令牌确实来自已登记的 Agent。下面的检查同时核对 Agent 身份,以及写操作需要的用户组和 scope。
REGISTERED_AGENTS = {"YOUR_AGENT_CLIENT_ID"}
def _require_agent() -> None:
claims = _decode_claims() # runtime 的 JWT authorizer 已经验证令牌
agent = (claims.get("act") or {}).get("sub")
if agent in REGISTERED_AGENTS and claims.get("cid") == agent:
return
raise ToolError("insufficient_privileges: this server accepts only tokens that a registered "
"AI agent obtained through Okta Cross App Access")
def _require_write() -> None:
_require_agent()
if WRITERS_GROUP in _groups() and "xaa:write" in _scopes():
return
raise ToolError(f"insufficient_privileges: this tool requires membership in '{WRITERS_GROUP}'")
关键是 WRITERS_GROUP in _groups() 与 "xaa:write" in _scopes() 必须同时成立。作者早期的实现对缺少 groups 的令牌放行,导致 Carol 也能创建任务;加入 Agent 身份检查之前,服务应用获取的普通 token-exchange token 没有 act claim,也曾替 Bob 创建成功。
这里能直接读取 claims,是因为前面的 runtime 已经验证 JWT。脱离这个入口,服务必须自行验证签名、issuer、audience 和有效期。
撤销授权也有时间边界。作者移除用户对 Agent 的分配后,约 1 秒内新请求被拒绝,但已签发令牌仍可存活至 5 分钟。更细的一点是,Okta 不会在兑换 ID-JAG 时重新检查用户:撤销前签发的 ID-JAG,在移除用户 3 秒后仍换出了 access token。ID-JAG 自身也是 5 分钟有效、单次使用。
约 1 秒拦住新委托,不等于约 1 秒清空所有已有访问能力。
性能方面,作者给出的端到端请求约为 30 秒,Identity 增加约 0.6 秒。10 次请求中,首次兑换约半秒,第二个 MCP 服务复用同一令牌,中位耗时为 58 毫秒;同一用户提交新的 ID-JAG,则获得新令牌。
我的判断
这套方案最有价值的,是把管理员的授权决定、Agent 的凭证保管和工具的业务权限拆清楚了。对已经使用 Okta、又要让多个 Agent 进入企业系统的团队,这比共享服务账号更容易表达责任边界。
但它没有消除权限设计工作。groups 能表达读者和写者,不代表天然覆盖项目归属、客户范围或字段权限。资源服务仍要完成自己的业务授权;Part 1 还需要在每个 MCP 服务重复实施检查。
与普通 RFC 8693 token exchange 的比较,也要保留条件。作者在自己的 Okta 配置下没有收到 act claim,但同时承认 Okta 文档展示了这一 claim。不能据此断言普通 token exchange 一律不能携带 Agent 身份或用户权限。
性能数字则是小样本实测。原文称延迟表采用五次运行的中位数,但提供的材料缺少完整表格,也缺少部分权限验证和撤销结果表,无法进一步核对分布。约 0.6 秒放在约 30 秒的任务里可以接受,放到频繁调用的短请求里,就需要重新测量。
此外,原文明确说片段不是完整可部署方案,样例面向非生产用途。Part 2 才会引入 AgentCore Gateway、Cedar、Amazon EKS 上的第二个 Agent,以及 Amazon CloudWatch 审计关联,这些不能算作本篇已经验证的能力。
能拿来做什么
- 如果正在做带 AI 操作入口的前端应用,可以先画清 BFF 登录、委托兑换和工具执行的边界,确认登录应用与 Agent 的关联关系,并把 Agent 密钥保存在 Secrets Manager。
- 如果正在实现 Node 或 Python 的 MCP 服务,应把 Agent 身份检查与用户业务权限检查分别落到代码中,尤其验证缺少
groups、缺少act、客户端身份不匹配时是否拒绝请求。 - 为撤权流程补上实际验证:分别测试新委托、已签发 ID-JAG 和已有 access token,记录它们各自的失效时间,再决定 5 分钟是否符合业务要求。
- 试用后及时清理资源:删除 CLI 创建的 CloudFormation stacks,用
aws bedrock-agentcore-control delete-oauth2-credential-provider --name okta-xaa-obo删除 provider,用delete-workload-identity删除自行创建的身份,并移除新增 IAM 与 Okta 配置,避免持续计费。