跳到主要内容
版本:1.9.0

MCP 交互式登录

使用交互式登录时,MCP 客户端只需配置一个 URL,无需其他任何内容:

{
"type": "http",
"url": "https://gateway.example.com/mcp/jira"
}

用户第一次打开或使用该工具时,会在同一个浏览器窗口中完成三步:登录身份提供商、授权上游 MCP 服务器、在同意页上允许该客户端。此后,客户端持有一个可自动续期的网关会话,网关则使用该用户自己的令牌调用上游,并按需刷新。用户不会再被询问。上游令牌绝不会到达客户端,网关会话令牌也绝不会到达上游。

交互式登录需要 1.8.0 之后发布的网关。

最小示例:Keycloak 和 Jira​

本示例让用户在 Keycloak 登录,并通过 mcp-atlassian 连接到 Jira。Keycloak 群组 jira-users 的成员可以使用这些工具。测试版本为 Keycloak 26.0、Jira Software 10.3.26、mcp-atlassian 0.23.1 和 Open WebUI 0.12.5。

本示例中网关的公开地址是 https://gateway.example.com。客户端连接到单服务器入口 https://gateway.example.com/mcp/jira。会话只在签发它的服务器入口上有效,因此聚合入口 /mcp 不提供这种登录方式。

创建 Keycloak 客户端​

在 Realm(此处为 corp)中创建一个名为 aisix-mcp 的 OpenID Connect 客户端:

  1. 打开 Client authentication 和 Standard flow。
  2. 将 https://gateway.example.com/mcp-oauth/login/callback 添加为有效的重定向 URI。
  3. 添加一个 Group Membership Mapper,将令牌声明名称设为 groups,关闭 Full group path,打开 Add to ID token。

在 Jira 中进入 Administration > Applications > Application links,创建一个 External application 链接,方向选择 Incoming:

  1. 将重定向 URL 设为 https://gateway.example.com/mcp-oauth/callback。
  2. 将 Scope 设为 READ。
  3. 保存生成的客户端 ID 和客户端密钥。

运行 mcp-atlassian​

只需设置 JIRA_URL 和 ATLASSIAN_OAUTH_ENABLE。启用 OAuth 后,该服务器会使用网关随每个请求发送的令牌:

export JIRA_URL="https://jira.example.com"
export ATLASSIAN_OAUTH_ENABLE="true"

mcp-atlassian --transport streamable-http --host 0.0.0.0 --port 9000 --path /mcp

这种配置还会列出 Confluence 工具。由于没有配置 Confluence URL,调用这些工具会失败。

配置网关​

网关需要五项配置:公开 URL、带 login 的信任提供商、可访问该服务器的调用方 API Key、从群组到该 Key 的声明映射,以及 MCP 服务器本身。

AISIX Enterprise 或 AISIX Cloud​

导出连接信息和前面步骤中的密钥:

export AISIX_CP="YOUR_AISIX_CONTROL_PLANE_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
export KEYCLOAK_CLIENT_SECRET="YOUR_KEYCLOAK_CLIENT_SECRET"
export JIRA_CLIENT_ID="YOUR_JIRA_CLIENT_ID"
export JIRA_CLIENT_SECRET="YOUR_JIRA_CLIENT_SECRET"

设置公开 URL 并创建信任提供商:

curl -sS -X PATCH "$AISIX_CP/environments/$ENV_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mcp_resource_url": "https://gateway.example.com/mcp"}'

curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/oidc_providers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "corp",
"issuer": "https://sso.example.com/realms/corp",
"audiences": ["aisix-mcp"],
"identity_claim": "preferred_username",
"login": {
"client_id": "aisix-mcp",
"client_secret": "'"$KEYCLOAK_CLIENT_SECRET"'"
}
}'

创建调用方 API Key,并将群组映射到它:

export API_KEY_ID=$(curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/api_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "jira-users",
"allowed_models": [],
"mcp_access": { "allow": ["jira__*"] }
}' | jq -r '.api_key.id')

curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/claim_mappings" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "jira-users",
"jwt_provider": "corp",
"priority": 100,
"match": [
{ "claim": "groups", "op": "contains", "values": ["jira-users"] }
],
"resolve": { "api_key_id": "'"$API_KEY_ID"'" }
}'

创建 MCP 服务器:

curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "jira",
"url": "https://mcp-atlassian.example.com/mcp",
"auth_type": "oauth2_authorization_code",
"client_id": "'"$JIRA_CLIENT_ID"'",
"secret": "'"$JIRA_CLIENT_SECRET"'",
"token_url": "https://jira.example.com/rest/oauth2/latest/token",
"authorization_url": "https://jira.example.com/rest/oauth2/latest/authorize",
"scopes": ["READ"],
"allowed_environments": ["'"$ENV_ID"'"]
}'

在控制台中,提供商表单有 网关登录 部分,MCP 服务器表单的认证类型中提供 OAuth 2.0(用户各自登录)。环境的 MCP 访问 页面设置资源 URL 后,网关登录 部分还会显示两个重定向 URI 供复制。

AISIX OSS​

将以下条目添加到 resources.yaml:

resources.yaml(交互式登录)
mcp_auth_settings:
- resource_url: https://gateway.example.com/mcp

oidc_providers:
- name: corp
issuer: https://sso.example.com/realms/corp
audiences: ["aisix-mcp"]
identity_claim: preferred_username
login:
client_id: aisix-mcp
client_secret: ${KEYCLOAK_CLIENT_SECRET}

api_keys:
- display_name: jira-users
key_env: JIRA_USERS_KEY
allowed_models: []
mcp_access:
allow: ["jira__*"]

claim_mappings:
- name: jira-users
jwt_provider: corp
priority: 100
match:
- claim: groups
op: contains
values: ["jira-users"]
resolve:
api_key: jira-users

mcp_servers:
- name: jira
url: https://mcp-atlassian.example.com/mcp
auth_type: oauth2_authorization_code
client_id: ${JIRA_CLIENT_ID}
secret: ${JIRA_CLIENT_SECRET}
token_url: https://jira.example.com/rest/oauth2/latest/token
authorization_url: https://jira.example.com/rest/oauth2/latest/authorize
scopes: ["READ"]

即使没有人出示,每个调用方 API Key 条目也需要凭证,因此还要设置 JIRA_USERS_KEY,但不要分发它。没有控制面的网关把登录数据保存在内存中。在依赖它之前,请参阅没有控制面的网关。

检查 Challenge​

不带凭证的请求会收到 401,其中指向该服务器自己的登录入口:

curl -sS -i -X POST "https://gateway.example.com/mcp/jira" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

响应带有以下响应头:

WWW-Authenticate: Bearer resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/jira"

连接客户端​

将 URL 提供给客户端。在 Open WebUI 中,在管理设置里添加工具,类型选 MCP,认证选 OAuth 2.1,然后选择 Register Client,并设置工具的可见性,让用户能看到它。Claude Code 和 MCP TypeScript SDK 也经过测试,用法相同。

客户端的重定向 URI 必须是 https,或回环地址上的 http。同意页会显示客户端要把用户送回的地址,因为这个页面正是保护点击了他人链接的用户的关键。

配置参考​

MCP 服务器​

auth_type: oauth2_authorization_code 可用于 type: mcp 服务器,不可用于 type: openapi 服务器。所有 OAuth 字段都是可选的:

字段省略时
client_id网关向上游注册一次,作为公共客户端。
secret网关作为公共客户端进行认证。secret 需要 client_id。
token_url从上游发现。
authorization_url从上游发现。
registration_url从上游发现,且仅在省略 client_id 时使用。
scopes网关请求上游受保护资源元数据中要求的 Scope。

发现过程依次读取上游的 RFC 9728 和 RFC 8414 元数据。配置了 authorization_url、token_url 和 client_id 的服务器完全不会请求元数据,Jira 示例就是如此。

信任提供商 login​

字段必填说明
login.client_id是在身份提供商注册的客户端。
login.client_secret否在请求体中发送给 Token 端点。公共客户端可省略。在 AISIX Cloud Admin API 中只写不读,在 aisix export 中为占位符。
login.scopes否在 openid 和提供商的 required_scopes 之外额外请求的 Scope。

该提供商需要 issuer,且不能使用 hmac_secret。端点来自 issuer 的 OIDC 发现文档。与所有 JWKS 提供商一样,audiences 不能为空。

请从该发现文档的 issuer 字段复制 issuer 的值。两者必须完全一致,包括末尾斜杠。如果文档中的值与之不同,新登录会失败并返回 500 和 mcp_oauth_misconfigured,网关日志会记录 the trust provider's discovery document names another issuer。控制面不读取发现文档,因此保存时无法发现这个问题。如果只差末尾斜杠,现有网关会话仍可继续使用,因此登录失败会先出现在新登录上。直接作为 Bearer Token 出示的 JWT 会被拒绝并返回 401,因为其 iss 会与 issuer 精确比较。

环境中已启用且带 login 的提供商数量,决定该环境中每个 oauth2_authorization_code 服务器的工作方式。这不是按服务器设置的:

已启用且带 login 的提供商结果
一个交互式登录,即本文所述方式。
没有中继模式。参见中继模式。
多于一个,或 login 所在的提供商没有 issuer 或带有 hmac_secretAISIX Cloud Admin API 以 400 拒绝保存。在资源文件中,加载时不会被拒绝,但登录会以 500 和 mcp_oauth_misconfigured 失败。

1.8.0 及更早版本的网关会忽略 login;只要环境中注册了这样的网关,保存时的响应就会带有 warnings 数组。这种网关无法加载 oauth2_authorization_code 本身,因此只要目标环境中注册了这样的网关,AISIX Cloud Admin API 就会以 422 和 DP_INCOMPATIBLE 拒绝保存该服务器。

Audience​

有两处不同的检查会读取 aud:

  • 作为 Bearer Token 出示的 JWT 只按 audiences 校验。mcp_auth_settings.resource_url 只用于发现,绝不会与 aud 比较。
  • 交互式登录时的 ID Token 按 login.client_id 校验。audiences 无需列出该值,这项检查也不会读取 audiences。

重定向 URI​

<base> 是去掉末尾 /mcp 的 resource_url,因此包含路径前缀。

注册项注册位置值
登录回调身份提供商<base>/mcp-oauth/login/callback
上游回调上游的 OAuth 客户端(配置了 client_id 时)<base>/mcp-oauth/callback

两者都必须精确匹配。请使用 https:网关只在 https 的 base 上给登录 Cookie 加 Secure 标记,Jira 的 OAuth 提供方也要求 https 的基础 URL。

如果前方网关按路径前缀路由,还要转发 <base>/mcp/{server}/、<base>/mcp-oauth/ 和 <base>/.well-known/oauth-protected-resource/mcp/{server}。有些客户端还会尝试前缀之外的 well-known 路径。这些 404 响应没有影响,因为客户端随后会访问 <base>/mcp/{server}/.well-known/openid-configuration。参见在按路径前缀路由的网关后运行。

Jira​

以下内容适用于测试版本 Jira Software 10.3.26:

  • Scope 是精确的大写字符串 READ,且为必填。
  • 访问令牌默认有效期为 3600 秒,由系统属性 atlassian.oauth2.provider.access.token.expiration.seconds 设置。网关在到期前 60 秒刷新。
  • mcp-atlassian 需要 JIRA_URL 和 ATLASSIAN_OAUTH_ENABLE=true。

Keycloak​

声明映射、绑定声明、标签声明和身份声明都只读取 ID Token。网关不调用 userinfo 端点,也不读取访问令牌,因此 Mapper 必须打开 Add to ID token。

请让声明保持精简。ID Token 的声明保存在登录流程的 Cookie 中,上限为 4000 字节。很长的群组列表可能超出上限,用户此时会看到错误页面,而不是得到会话。只为网关的客户端配置它需要的 Mapper。

没有控制面的网关​

没有 dp-manager 的网关把登录数据保存在自身内存中。这些数据在重启后丢失,也不共享,因此会话和上游凭证只属于单个网关进程。在这种模式下使用交互式登录时,请只运行一个进程。

AISIX Enterprise 和 AISIX Cloud​

上游凭证通过 dp-manager 存储,并用部署的主密钥加密,因此同一环境中的所有网关共享这些凭证,并接受彼此的会话。

MCP 服务器的 已连接用户 面板列出已连接的用户,并提供 断开连接 和 全部断开。对应的 API 是 GET /mcp_servers/{id}/credentials、DELETE /mcp_servers/{id}/credentials/{credential_id} 和 DELETE /mcp_servers/{id}/credentials。这些操作需要 mcp_server_credentials 权限。该权限独立于 mcp_servers,普通成员不具备,因为列表中显示的是其他人的身份。

对于自托管部署,每个 dp-manager 副本会为此存储多打开最多 8 个 PostgreSQL 连接。这个数量是固定的。参见 PostgreSQL。

中继模式​

没有 login 提供商时,同一个服务器需要客户端提供网关凭证,登录只覆盖上游。客户端在 x-aisix-api-key 中发送网关凭证(包括在 Token 请求上),并收到一个本身没有任何权限的占位令牌。上游令牌保留在网关中,按调用方身份存储。该凭证必须能指明用户:JWT,或有所有者的 API Key。其他凭证会收到 403 和 mcp_oauth_no_subject。

行为​

会话​

访问令牌有效期为 1 小时。刷新令牌有效期为 14 天,每次刷新都重新计算 14 天,没有总时长上限。刷新令牌只能使用一次。刷新时网关不会回查身份提供商,因此只要 Key 和映射允许,经常使用的客户端会一直保持登录。

同意页有 Allow 和 Cancel。每次新登录都以它结束,包括用户已经授权过上游的情况。Cancel 会拒绝该客户端,客户端收到 access_denied,不会获得会话。

对于通过网关登录的用户,Cancel 不会撤销其刚刚在上游给出的授权。上游令牌仍保留在存储中,已连接用户 面板会显示其已连接,其下一次登录也不会再要求上游授权。管理员可以通过断开该用户的连接移除它。在上游自己的页面上拒绝授权则不会存储任何内容。

Key、声明映射或提供商的变更在下一个请求生效,并基于登录时记录的声明求值。从声明读取的标签也是如此:身份提供商上变更的值要等用户重新登录才生效,因为刷新不会更新已记录的声明。

拒绝​

情况登录过程中带会话访问 /mcp/{server} 时
身份未映射到任何 Key403 页面401 jwt_identity_unmapped
Key 已禁用或已过期403 页面401 api_key_disabled 或 api_key_expired
不满足 bound_claims403 页面403 jwt_claims_rejected
未授予 required_scopes403 页面不适用
Key 无权访问该服务器403 页面没有 HTTP 拒绝
会话无效或已过期、其提供商已删除或已禁用,或提供商的 issuer 已变更不适用401 mcp_oauth_authorization_required
上游凭证已不存在用户重新授权上游401 mcp_oauth_authorization_required
存储或上游 Token 端点不可用503 页面503 mcp_oauth_unavailable

每个 401 都带有含 resource_metadata 的 WWW-Authenticate。登录拒绝页面会显示身份,refused a sign-in 日志行同样如此,用户可以把它告诉管理员。

Key 在登录后失去对该服务器的访问权限时,不会产生 HTTP 拒绝。initialize 成功,tools/list 中没有这些工具,tools/call 返回 JSON-RPC invalid_params 错误。恢复访问权限后无需重新登录。

上游撤销​

网关在上游返回 401 时,或在下一次刷新令牌时,才会发现授权已被撤销。Jira 不返回 401:它把失效令牌当作匿名请求并返回 200。因此工具可能看起来是空的,而不是报错,模型也只会说没有找到内容。网关要到下一次刷新才会发现,按 Jira 的默认有效期,最长约 59 分钟后。

Jira 还会继续为已停用的用户刷新令牌,因此该用户永远不会被提示。请按离职处理中的说明清除其凭证。

Open WebUI 恢复​

存储的凭证不存在时,Open WebUI 显示 Failed to connect to MCP server,模型也会说没有这个工具。要立即恢复,请在该工具上选择 Disconnect,然后重新打开它,浏览器流程会再次运行。

否则,恢复可能需要几十分钟,测试中为 41 分钟。Open WebUI 收到 401 时不会发起登录,只在会话即将到期前才刷新,正是这次刷新把它送回浏览器。

Open WebUI 中的 Disconnect 只清除客户端自己的会话,不会通知网关。如果上游凭证仍然有效,重新打开工具时只会显示同意页。

断开用户连接​

操作方方式
通过网关登录的用户无法自行断开。DELETE /mcp/{server}/oauth/credential 需要网关凭证,不接受会话令牌。
中继模式的调用方使用自己的网关凭证调用 DELETE /mcp/{server}/oauth/credential。
管理员(AISIX Enterprise 或 AISIX Cloud)已连接用户 面板或 Admin API。
管理员(没有控制面的网关)只能重启,这会让所有人退出登录。

断开连接只移除存储的凭证,不会撤销上游的授权,因此 Jira 中仍会列出该授权。

断开连接不会结束网关会话,因为会话是无状态的。之前签发的会话停止工作,只是因为凭证已不存在,所以同一用户重新授权后,该会话会恢复可用,也可以继续刷新,并使用该用户自己的新上游令牌。要阻止某人继续访问,还需要让其无法登录。禁用其 Key 可以做到这一点,并会在下一个请求拒绝其会话,该 Key 服务的其他所有人也会被拒绝。在身份提供商处移除该用户不影响现有会话,但会让恢复凭证所需的登录失败,因此要在断开连接之前完成。

令牌在本地缓存中的网关,最多还可以在请求中继续使用它 5 分钟,这 5 分钟从该网关缓存令牌时算起,而不是从断开连接时算起。此后请求会收到 401。唯一的例外是存储不可用期间:网关无法核对,因此会继续使用已持有的令牌,不受这 5 分钟限制,直到令牌过期。新登录和会话刷新都读取存储,因此断开连接后立即登录会再次要求上游授权,刷新则会被拒绝并返回 invalid_grant,用户随之回到浏览器流程。

离职处理​

在身份提供商中停用用户或将其移出群组,不会影响现有会话,因为会话携带的是登录时的声明,也不会回查。管理员需要在每个服务器上清除该用户的凭证。下一次刷新会失败,而新的登录会遵循身份提供商当前的结果。

要立即切断某个 Key 所服务的所有人的访问,请禁用该 Key 或删除声明映射。这在下一个请求生效。

哪些变更会断开用户​

在 AISIX Enterprise 和 AISIX Cloud 中,以下变更会移除存储的上游凭证:

  • 修改 MCP 服务器的 url、client_id、token_url、authorization_url、registration_url 或 auth_type,会断开该服务器的所有用户。
  • 删除服务器或其所在环境会移除凭证;将服务器从某个环境移除,会移除该环境的凭证。
  • 删除 OIDC 提供商或修改其 issuer、identity_claim,会使其用户重新授权每个 MCP 服务器,其中通过网关登录的用户还需要重新登录。只增减 issuer 末尾斜杠不会清除存储的令牌,也不会让任何人退出登录,但可能导致新登录失败,并使直接出示的 JWT 被拒绝。

其他修改(例如 scopes、新的 secret、jwks_uri 或 hmac_secret)不会断开任何人。

删除唯一带 login 的提供商后,环境进入中继模式,只有配置了新的带 login 的提供商,登录才能恢复。禁用该提供商在其禁用期间效果相同,但不会移除凭证。

修改 client_id 而不发送新密钥时,旧密钥会被移除,MCP 服务器和 login 都是如此,因为该密钥属于旧客户端。

没有控制面的网关不会移除任何内容。此时修改 url、token_url、issuer、identity_claim 或用户的身份值,会使其存储的凭证无法再被访问,用户需要重新授权上游。删除提供商会结束其会话。

在这两种部署形态下,修改 identity_claim 后,即使用户在新声明下的值没有变化,其存储的凭证也无法再被访问。这类用户的会话在下一个请求上会收到 401 质询,刷新时会收到 invalid_grant,用户需要重新授权上游。

主密钥​

对于自托管部署,存储的记录最长保留 365 天,没有任务会用新主密钥重新加密它们。记录只有在网关下一次写入时才会用活动密钥重写,环境的会话密钥可能要等很多个月。退役某个密钥前,请考虑用它加密的记录将无法读取。读取这些记录会返回 503,受影响的用户和登录会收到 mcp_oauth_unavailable,而不是新的提示。请按密钥中的说明保留每个已退役的密钥。

出站请求​

outbound.ssrf_protection 管理运维人员未选择的 URL。管理员配置的 URL 不受其限制:issuer 发现及其指向的端点、token_url、registration_url 和服务器的 url,以及与该 url 同一 authority 上的任何元数据。内网身份提供商或上游无需配置任何条目即可工作。

上游元数据指向另一个 authority(协议、主机和端口)的端点或文档会受其限制,因此如果位于私有地址,就需要显式配置或添加 allowed_hosts 条目。这些请求绝不跟随重定向。参见配置文件。

可观测性​

aisix_auth_decisions_total 以 method="mcp_oauth_session" 和 method="mcp_oauth_login" 记录这些决策。因提供商的 issuer 变更而被拒绝的会话,其 reason 为 session_issuer_changed。参见指标参考。

用量事件在 jwt_subject 中携带用户身份,即使多个用户共用一个 Key 也是如此。共用 Key 的访问日志行无法区分用户。

后续步骤​