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 客户端:
- 打开 Client authentication 和 Standard flow。
- 将
https://gateway.example.com/mcp-oauth/login/callback添加为有效的重定向 URI。 - 添加一个 Group Membership Mapper,将令牌声明名称设为
groups,关闭 Full group path,打开 Add to ID token。
创建 Jira 链接
在 Jira 中进入 Administration > Applications > Application links,创建一个 External application 链接,方向选择 Incoming:
- 将重定向 URL 设为
https://gateway.example.com/mcp-oauth/callback。 - 将 Scope 设为
READ。 - 保存生成的客户端 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:
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_secret | AISIX 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} 时 |
|---|---|---|
| 身份未映射到任何 Key | 403 页面 | 401 jwt_identity_unmapped |
| Key 已禁用或已过期 | 403 页面 | 401 api_key_disabled 或 api_key_expired |
不满足 bound_claims | 403 页面 | 403 jwt_claims_rejected |
未授予 required_scopes | 403 页面 | 不适用 |
| 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 的访问日志行无法区分用户。