AuthKeystone IDaaS 下游接入指南
AuthKeystone 是 OIDC 身份提供方(IDaaS),下游业务服务(ExoMind/wechat/…)通过标准 OIDC 接入。 本文档是下游接入的契约与规范——照着做,身份层零踩坑。定稿 2026-07-11。
1. 10 分钟快速接入
按以下顺序完成一次最小可用的 Web 接入。生产环境优先使用标准 OIDC SDK;它会通过发现文档读取端点,避免硬编码。
- 在后台创建 OAuth client,登记精确的
redirect_uri;服务端 Web 应用保存client_secret,SPA 使用 PKCE。 - 按需启用
openid profile email等 scope。需要邮箱不代表邮箱已验证,见第 7 节。 - 从
https://auth.ai-as.cc/.well-known/openid-configuration发现端点,跳转授权端点并生成、保存state和 PKCE 参数。 - 回调时先校验
state,再用授权码换取 token;回调中的error也必须处理并展示给用户。 - 用 JWKS 验签 token,并校验
iss、aud(必须等于自己的client_id)、有效期和 nonce(使用时)。 - 用
sub建立或查找下游用户,创建自己的业务 session;不要把 token 当作永久 session。
最小流程:
浏览器 -> /authorize -> AuthKeystone 登录 -> 业务 callback
业务后端 -> /oauth/token -> 验签与校验 aud -> 业务 session
2. 四层身份模型
AuthKeystone 的身份架构是四层正交,下游接入前必须分清:
| 层 | 字段 | 语义 | 下游怎么用 |
|---|---|---|---|
| 身份(Who) | sub | 用户唯一稳定标识(= user_id,跨改名/换组织不变) | 用户主键:个人知识库、配额、偏好、会话——所有「按用户隔离」的场景都用它 |
| 组织(Whose) | org_id | 组织容器(个人/团队/企业) | 仅团队/企业数据隔离;个人场景不需要。绝不用 org_id 当用户身份 |
| 权限(What) | permissions | RBAC 权限列表 | 授权判断(或调 auth check_permission) |
| 客户端(Which) | aud | 目标业务(client_id) | 验 token 时校验 aud == 自己的 client_id,防 token 跨业务误用 |
铁律:sub = 用户身份主键,org_id = 组织容器。两者正交,绝不混用。
- ❌ 错误:用 org_id 做个人知识库 key(org_id=‘default’ 对所有 default 用户一样 → 串用户)
- ✅ 正确:用 sub 做个人知识库 key(每人唯一),org_id 仅做团队空间隔离
3. 接入流程
3.1 在 auth 注册 OAuth client
平台管理员在 AuthKeystone 后台为你的服务创建 OAuth client,得到:
client_id/client_secret(机密客户端)或仅client_id(公开客户端,前端 SPA,强制 PKCE)redirect_uri(你的回调地址)grant_types(authorization_code / refresh_token / password / client_credentials;Agent 场景加 token-exchange / device_code,见「Token Exchange 与 Agent 身份」「设备授权流」)
3.2 声明用哪些 scope(字段契约)
在 AuthKeystone 后台「OAuth 客户端 → Scope 配置」勾选你的服务需要的 scope。每个 scope 对应一组 claim(token 字段):
| scope | 暴露的 claim | 谁该要 |
|---|---|---|
openid(默认,所有 client) | sub | 所有 |
profile | preferred_username, name | 要展示用户名的(picture 头像字段规划中,暂不签发) |
email | email, email_verified | 要邮箱的 |
auth:permissions | permissions[] | 要做 RBAC 授权的 |
auth:org | org_id | 要做团队/企业隔离的(个人场景不要) |
业务自定义(如 exomind:wiki) | 业务约定 | 特定业务,向 SaaS 申请 |
不声明的 scope → token 里没有对应 claim(最小暴露)。例如只要 openid + profile → token 只有 sub + preferred_username,拿不到 org_id/email/permissions。
email_verified=true;sub才是唯一稳定主键。
3.3 OIDC 授权码流程(Web 应用,推荐)
用户访问你的服务
→ 你重定向到 AuthKeystone 的 /authorize:
https://auth.ai-as.cc/authorize?
client_id=你的client_id
&redirect_uri=你的回调
&response_type=code
&scope=openid+profile+email(你需要的)
&state=随机(防CSRF)
&code_challenge=PKCE(公开客户端必填)
→ 用户在 auth 登录(或已登录则自动 SSO)
→ auth 回调你的 redirect_uri?code=xxx&state=xxx
→ 你的后端用 code 换 token:
POST https://auth.ai-as.cc/oauth/token
grant_type=authorization_code
code=xxx
client_id / client_secret(机密)或 code_verifier(公开 PKCE)
→ 返回 {access_token, id_token, refresh_token}
→ 你的后端用 JWKS 公钥本地验签 id_token/access_token
→ 取 sub(user_id)做用户身份 + 按 permissions 授权
→ 发你自己的 session cookie
按需预热认证域名连接
用户点“AuthKeystone 登录“时,浏览器要从你的域名跳转到 auth.ai-as.cc。若此前没访问过该域名(或 DNS 缓存已过期),浏览器要现做 DNS 解析 + TCP 建连 + TLS 握手;跨公网、尤其移动网络 / 运营商 DNS 抖动时,这一步可能耗时 1 秒以上,用户会感觉“点了登录卡一下“。
当登录入口可见、且认证域名与业务域名不同,可在页面 <head> 中加入:
<link rel="dns-prefetch" href="//auth.ai-as.cc">
<link rel="preconnect" href="https://auth.ai-as.cc" crossorigin>
dns-prefetch预先解析认证域名。preconnect还会建立 TCP/TLS 连接,通常能缩短首次登录跳转。
把它放在登录入口所在页面或全局 layout;低频使用的管理入口无需强制添加。preconnect 会增加一次额外连接,应结合实际登录频率和性能数据启用。
静默 SSO 探测(prompt=none)
如果业务想在不打扰用户的前提下判断“是否还登录着“(SPA 启动静默续签、隐藏 iframe 刷新会话),给 /authorize 加 prompt=none:
https://auth.ai-as.cc/authorize?
client_id=你的client_id
&redirect_uri=你的回调
&response_type=code
&scope=openid
&prompt=none
- AuthKeystone 侧 SSO cookie(
auth_session)仍有效 → 静默签发 code,回调照常换 token,用户无感 - SSO cookie 已失效 → 不弹登录页,改为回调
error=login_required,前端据此决定跳登录还是保持未登录态
注意:prompt=none 是精确匹配——缺省、空串或 consent/select_account 等其它值一律落入标准授权码流程(该弹登录页就弹),不发 prompt 的 client 行为完全不变。none 与其他值组合(如 none login)是规范违例,回调 error=invalid_request。
强制重新认证(prompt=login,「切换账号」入口)
业务侧用户已登录,但想提供切换账号能力(头像菜单里「切换账号」按钮、多账号工作台等),给 /authorize 加 prompt=login:
https://auth.ai-as.cc/authorize?
client_id=你的client_id
&redirect_uri=你的回调
&response_type=code
&scope=openid
&prompt=login
- AuthKeystone 忽略现有 SSO cookie(无论是否仍有效),一律 302 到登录页要求重新认证
- 用户以新身份登录后自动跳回
/authorize完成授权,业务回调拿到新用户的 code,覆盖本地旧会话——这是 RP 侧替换老登录态的标准方式(IdP 不会主动跨域踢掉 RP 的既有会话) - 回跳时 AuthKeystone 自动剥掉
prompt=login参数,登录完成后按标准流程签 code,不会循环要求登录
典型接入:切换账号按钮直接 location.href = 上面的 authorize URL(带 state),其余与普通登录完全一致,无需新回调逻辑。
同域第一方 SPA:用 SSO cookie 兑换本地会话(/api/auth/sso-resume)
用户在业务侧(RP)完成登录后,AuthKeystone 在浏览器种了 HttpOnly 的 auth_session SSO cookie——但跨域的第三方站读不到它,同域的第一方 SPA 也没有本地 token。部署在 AuthKeystone 同域的第一方 SPA(如 admin 后台)想在启动时免密恢复本地会话:
POST /api/auth/sso-resume # 无 body;同源 fetch 自动带 auth_session cookie
- cookie 有效(服务端 SSO 会话存在且未被撤销)→
200,返回与登录同构的完整 token 对(access_token/refresh_token/user/permissions),签的是全新 token,并轮换下发新 cookie(滑窗续期) - 无 cookie / 会话失效(已登出、被踢线、会话过期)→
401,前端静默落回登录页即可——cookie 是 HttpOnly,JS 无法预判存在与否,失败是常态不是错误
cookie 语义:auth_session 的值是 opaque 会话句柄(sso_*),对应 AuthKeystone 服务端会话(Redis),有效期与 cookie 本体一致(默认 7 天)且每次使用滑动续期——不随某个 access_token 过期而失效(历史上 cookie 直接装 access_token,24h token 到期后 cookie 变砖、SSO 全线失效,已废弃)。登出 / 踢线 / 改密会即刻删除服务端会话,cookie 立即失效。
典型接入:SPA bootstrap 无本地 token 时先调它试一次,成功即恢复会话;本地 token 校验与续期双双失败时也应再试一次兜底(cookie 可能比本地 token 新,如刚在 RP 登录过——auth-client bootstrap() 已内置两条路径)。第三方跨域站点不适用(fetch 不带别站 cookie),仍走标准授权码流程。
3.4 登出与单点登出(SLO)
先由产品明确退出语义,再选择实现:
- 本地登出:清理业务自身 session。用户再次进入业务时,若 AuthKeystone 的 SSO 会话仍有效,可能被无感登录。
- 全局登出:浏览器跳转 AuthKeystone 的
/oidc/end_session,清除 AuthKeystone SSO 会话;这会影响同一浏览器中其他接入 AuthKeystone 的应用。
需要用户明确退出整个身份会话时,使用全局登出;仅离开当前业务时,本地登出即可。
为什么会无感重新登录:首次登录后 AuthKeystone 会在浏览器种一个 HttpOnly 的 auth_session SSO cookie(服务端会话句柄,默认 7 天滑动续期)。本地登出不调 end_session 时:
auth_sessioncookie 残留在浏览器- 用户再点登录 →
/authorize读到 cookie 有效 → 免密直接发 code(“自动登录”) - token 未过期(24h 内)就一直免密;过 24h 才重新要密码——这就是“有时候自动登录“的根因
全局登出怎么调(业务 logout 最后一步,浏览器顶层跳转):
https://auth.ai-as.cc/oidc/end_session?
id_token_hint=<登录时拿到的 id_token>
&post_logout_redirect_uri=<登出后回业务应用的地址>
| 参数 | 必填 | 作用 |
|---|---|---|
id_token_hint | 推荐 | 登录时 /oauth/token 返回的 id_token。带它,auth 撤销该用户 access/refresh token + 删 session;不带只清 cookie。回跳白名单按 hint 定位 client,不带则无法校验白名单 |
post_logout_redirect_uri | 可选 | 登出后回业务应用的地址(须 http(s)://)。必须提前在 OAuth client 的「登出回跳白名单」(post_logout_redirect_uris)注册,精确匹配(含尾斜杠差异,https://youhuale.cn ≠ https://youhuale.cn/);未注册 / 不匹配时 auth 不回跳,302 落 auth 首页并记 SLO_REDIRECT_REJECTED 审计 |
- 必须
window.location浏览器跳转,不能fetch——跨域请求不带 auth.ai-as.cc 的 cookie,清不掉auth_session。 - 标准库可自动:发现文档已声明
end_session_endpoint,标准 OIDC SDK(Nodeopenid-client/ Pythonauthlib/ Javapac4j)发现后会自动在 logout 调用,通常无需手写。 - 登出后 auth 后台仍显示登录态是正常的:auth 第一方后台是独立的 localStorage token 会话(双会话体系),不随 SSO cookie 清除失效,与 SLO 无关。
3.5 验 token(JWKS 本地验签,推荐)vs introspect(实时查)
- 本地验签(推荐):用
/.well-known/jwks.json的 RS256 公钥本地验 token 签名 + 校验aud=你的 client_id +exp未过期。无状态,高性能。 - introspect(备选):
POST /oauth/introspect实时查 token 有效性 + 权限。适合需要 token 实时状态(如已吊销)的场景。
3.5.1 JWKS 多密钥与轮换(kid)
/.well-known/jwks.json 输出多把 RS256 公钥(kid 各异):激活密钥在前,其后为退休密钥。验签时按 token header 的 kid 选公钥(标准 JWT 库默认行为),不要取第一把硬验。
管理员执行密钥轮换(后台「系统设置」权限)后:新 token 立即用新 kid 签发;旧密钥在宽限期(默认 7 天)内保留在 JWKS,旧 token 持续可验,不需要把在线用户踢下线。宽限期结束后旧密钥从 JWKS 移除。下游按 RFC 7517 常规缓存 JWKS 即可(缓存期建议 ≤24h,遇未知 kid 时强制刷新一次再验)。
3.6 用标准 OIDC 库接入(推荐,省去手写流程)
上面的手写流程是原理参考。生产接入直接用标准 OIDC 库:配
authority = issuer,库自动拉发现文档拿所有端点和能力,零硬编码端点。不需要@authkeystone专属 SDK——AuthKeystone 是标准 OIDC,标准库最合适(零锁定、社区维护、未来换 IdP 不改代码)。
前端 SPA(oidc-client-ts):
import { UserManager } from 'oidc-client-ts'
const mgr = new UserManager({
authority: 'https://auth.ai-as.cc', // = issuer,自动拉发现文档
client_id: '你的client_id',
redirect_uri: 'https://你的域名/cb',
scope: 'openid profile email', // 按需加 auth:permissions / auth:org
})
mgr.signinRedirect() // 登录
await mgr.signinCallback() // 收 callback 换 token
await mgr.signinSilent() // 静默续期(自动 refresh)
mgr.signoutRedirect() // 登出(自动走 end_session_endpoint)
后端(Node openid-client / Python authlib):
// Node:Issuer.discover 自动拉发现文档
const { Issuer } = require('openid-client')
const issuer = await Issuer.discover('https://auth.ai-as.cc')
const client = new issuer.Client({ client_id, client_secret, redirect_uris: ['https://你的域名/cb'], response_types: ['code'] })
const url = client.authorizationUrl({ scope: 'openid profile email', state, code_challenge, code_challenge_method: 'S256' })
const tokenSet = await client.callback('https://你的域名/cb', cbParams, { code_verifier, state })
await client.refresh(tokenSet.refresh_token) // 刷新(重查 permissions)
client.endSessionUrl({ id_token_hint: tokenSet.id_token }) // 登出
scope 与 claim
- 标准:
openid(sub)/profile(preferred_username,name)/email(email,email_verified) - AuthKeystone 扩展:
auth:permissions(permissions[],做 RBAC)/auth:org(org_id,组织隔离)。按需,在后台「客户端 Scope 配置」勾选。
⚠️ permissions 的时效性(重要)
permissions 固化在 token 里(无状态本地验签,高性能)。使用注意:
- 改权限对 auth 自身 API 即时生效:admin 改角色菜单 → AuthKeystone 自动撤销受影响用户的 session(
invalidate_sessions_for_users反查该角色所有用户)→ 旧 token 失效。 - 下游本地验签有 TTL 窗口:你用 JWKS 本地验签(只验签名、不查 session)时,session 撤销对你不生效——旧 token 的旧 permissions 在 TTL 内仍被你判为有效。这是本地验签高性能的固有代价(所有 permissions-in-token 的 IDaaS 通用)。
- 建议:高频低敏操作用 token 里的 permissions;敏感授权调
POST /oauth/introspect或POST /api/permissions/check实时查,或用短 TTL + 频繁 refresh(refresh 时 AuthKeystone 重查 permissions 更新)。
3.7 动态客户端注册(RFC 7591/7592)
下游服务自助注册 client,无需管理员手动创建:
POST https://auth.ai-as.cc/oauth/register
Content-Type: application/json
{
"client_name": "my-mcp-cli",
"redirect_uris": ["http://127.0.0.1:8300/callback"],
"grant_types": ["authorization_code", "urn:ietf:params:oauth:grant-type:device_code"],
"token_endpoint_auth_method": "none",
"scope": "openid profile"
}
响应返回 client_id、client_secret(机密 client)与 registration_access_token。用该 token 调 GET/PUT/DELETE /oauth/register/{client_id} 管理(RFC 7592),registration access token 有效期 30 天。可注册的 scope 限定 openid profile email。
4. OIDC 端点速查
| 端点 | 用途 |
|---|---|
GET /.well-known/openid-configuration | OIDC 发现文档(所有端点 + 支持的 scope/claim/grant) |
GET /.well-known/oauth-authorization-server | RFC 8414 AS Metadata(纯 OAuth2 / MCP 接入方发现 AS,内容与 OIDC 发现一致) |
GET /.well-known/jwks.json | RS256 验签公钥(本地验 token) |
GET /authorize | 授权码签发(用户登录入口) |
POST /oauth/token | 换 token(authorization_code / password / refresh_token / client_credentials / token-exchange / device_code) |
POST /oauth/device/authorize | 设备授权签发(RFC 8628,见「设备授权流」) |
POST /oauth/register | 动态客户端注册(RFC 7591,见 3.7) |
POST /api/authorize | Agent 资源授权实时决策(见「Agent 资源授权」) |
GET /api/userinfo | 用户信息(Bearer token → 标准 OIDC claims) |
POST /oauth/introspect | token 内省(实时查有效性 + 权限) |
POST /oauth/revoke | token 吊销 |
GET /oidc/end_session | 单点登出(SLO) |
5. token claim 详解
核心层(所有 token 必有,OIDC 协议级)
{
"sub": "1", // user_id(字符串),用户唯一稳定主键
"iss": "https://auth.ai-as.cc",
"aud": "your_client_id", // 你的 client_id(验 token 时校验)
"exp": 1735689600,
"iat": 1735603200,
"jti": "unique-token-id"
}
aud已签发,直接开校验即可:自 2026-07-04 起,所有经/oauth/token签发的 token 都带aud= 你的client_id——authorization_code / password / refresh_token / client_credentials 四种 grant 的access_token+id_token全覆盖,服务端无需额外配置。若你方代码里仍有「JWT 无 aud claim」之类注释,是早期接入遗留的过时认知,以实际 token payload 为准(抓一个 token decode 即可见aud)。下游开启 audience 校验三步:
- 验签 claims 结构加
aud字段(access_token 可选、id_token 必填,统一用Option<String>接收即可)。- 期望值设为你自己的
client_id。- 验签时打开
validate_aud = true。走
/oauth/token(RP 正常接入的入口)拿到的 token,aud必为你的client_id。
⚠️ 第一方用户 token(aud = issuer)——下游验签必须纳入白名单(2026-09-04 修正)
auth 的用户直登端点(网页登录、微信小程序登录、短信登录等)签发的 access_token,aud 固定为 issuer(https://auth.ai-as.cc)——这是 auth 的第一方用户会话凭证(Auth0 给自家 API 签的 token 同款形态),会被你的下游场景直接消费(典型:ExoMind 小程序端拿它直调 /ingest /query)。下游 verify_jwt 若 fail-closed 只认 aud == client_id,这类 token 全部 401(ExoMind 实锤踩过)。
正确校验(一行改动):
if aud not in (YOUR_CLIENT_ID, "https://auth.ai-as.cc"): # 白名单
return 401
安全边界不变:issuer-aud token 仍是同一 issuer 签名(JWKS 验签照做)、iss 校验照做、sub/permissions 语义与授权码流程完全一致。若需严格的 per-RS audience(如 Agent 委托链),走 token exchange(§11)换 aud=resource 的委托 token。
| token 形态 | 签发入口 | aud | 下游怎么验 |
|---|---|---|---|
| client / 授权码 / password grant token | /oauth/token | 你的 client_id | aud == client_id |
| 第一方用户会话 token(网页/小程序/短信登录) | auth 登录端点 | issuer | 白名单 {client_id, issuer} |
| 委托 token(OBO) | token exchange | resource 指示器 | 按需 |
扩展层(按 scope 暴露)
{
"preferred_username": "zhangsan", // scope=profile
"email": "zs@example.com", // scope=email
"email_verified": true,
"org_id": "default", // scope=auth:org(个人场景别要这个)
"permissions": ["wiki:read", "wiki:write"], // scope=auth:permissions
"is_anonymous": true // 匿名访客标记(仅匿名账号携带;正式账号不输出该字段)
}
委托层(token exchange 签发的委托 token 携带)
{
"act": { // RFC 8693 §4.1 委托链,最外层 = 当前 actor
"iss": "https://auth.ai-as.cc",
"sub": "agent_manager", // 代表用户行事的 Agent client
"agent_id": "agent_007" // Agent 平台侧 agent 主键(绑定时才有)
},
"agent_id": "agent_007", // 扁平便捷 claim(工具/网关直读)
"token_type": "delegation", // sub 仍为被代表的用户(OBO 语义)
"aud": "https://mcp.erp/api" // resource 指示器(RFC 8707),未传时继承原 aud
}
用法见 Token Exchange 与 Agent 身份。
匿名访客(is_anonymous,2026-08-15 起)
AuthKeystone 支持匿名登录(POST /api/auth/anonymous,对标 Supabase Anonymous Auth):访客不注册即拿到正常 JWT 与只读能力,is_anonymous=true 标记在 claim 里(正式账号不输出该字段)。
平台侧门禁(无需下游配合):匿名账号不可 建 API Key / 注册 OAuth client / 申请角色提升——统一 403 + 提示转正。
下游建议:对写操作自查 is_anonymous,true 时引导转正(POST /api/auth/upgrade 绑邮箱,user_id 不变、数据保留)。转正后新签 token 即无此标记。闲置 30 天未转正且无资产的匿名账号由平台自动清理。
用户角色提升申请(SaaS 自服务,2026-08-15 起)
组织内用户可对本组织的自定义角色自助发起申请(POST /api/role-promotions),由本组织 system:role:approve 持有者(org_admin / 角色审批员)或平台 admin 审批。对下游的影响:用户 permissions 可能经审批增长——token 内 permissions 最长 24h 刷新,需即时感知的场景走 introspect 或 /api/userinfo。
角色有涵盖关系(如 ExoMind管理员 ⊃ 读写 ⊃ 只读,按菜单集推导):分配/申请均拒绝冗余组合,选高阶时其涵盖的低阶自动不可选。
claim 拿不拿得到,分两层看:
- 「discovery 有」≠「你的 token 有」:
scopes_supported/claims_supported声明的是 AuthKeystone 的全局能力,某 client 实际拿得到哪些取决于后台给该 client 勾选的 scope(按「client 启用 ∩ 请求」投影,没勾的 scope 对应 claim 静默不发、不报错)。拿不到先查后台 scope 勾选。name/email_verified只在/api/userinfo返,不进 token:access_token/id_token 的 claim 集不含这俩,需要必须调 userinfo,不能只本地验 token。
refresh token 轮换与重用检测(OAuth 2.1 合规)
AuthKeystone 对 refresh token 实行轮换 + 重用检测(OAuth 2.1 §4.3.1 / MCP 规范强制项):
- 每次 refresh 换发新 refresh token,旧 refresh token 立即失效。客户端必须用最近一次
/oauth/token(或/api/auth/refresh)返回的refresh_token,丢弃旧的。 - 旧 refresh token 被再次提交 → 判定泄露 → 立即吊销整条链(该用户当前 access token 进黑名单 + 所有新旧 refresh 失效),强制重新登录。这是防盗刷的安全机制:攻击者若拿了你已用过的 refresh token 去用,会触发你这条登录链自毁,迫使重登(你会察觉异常)。
- 跨端点隔离:OAuth 端点(
/oauth/token)签发的 refresh token 只能由该端点 + 对应 client 兑换,不能拿去/api/auth/refresh(admin-ui 旧端点);反之亦然。 - 客户端实现要点:拿到新 refresh token 立即替换存储;不要并发用同一个 refresh 发两次(并发的第二个会被当作重用,触发整链吊销)。标准 OIDC SDK 默认串行 refresh,通常无需特别处理。
上线兼容:本功能上线前签发的 refresh token,在宽限期内首次兑换会平滑过渡(建立家族记录),不会一上线就把用户踢下线。
6. 关键规范
- 用
sub做用户主键——这是 OIDC 标准,跨组织/改名/换租户都稳定。知识库、配额、会话、偏好,全用它。 org_id仅用于团队/企业隔离——做团队空间、企业数据隔离。个人场景(每个用户独立空间)用sub,不要org_id。- 验 token 时校验
aud= 你的 client_id——防止别的业务的 token 被你误用。 - 只声明你需要的 scope——最小暴露,AuthKeystone 会追踪谁用了哪些字段(迭代时评估影响)。
- 业务自定义 scope 走申请制——向 auth SaaS 管理员申请(如
exomind:wiki),不要擅自用未注册的 scope。 - 按产品语义选择登出方式——本地登出清业务 session;需要退出整个 SSO 会话时,浏览器跳
/oidc/end_session(带id_token_hint)。详见 3.4。
7. API Key(服务间,machine-to-machine)
如果你的服务是后端调用 auth(无用户交互),用 API Key:
- 在 AuthKeystone 后台创建 API Key(绑权限范围 + 可选 account_key 资源隔离)。
- 调用时
Authorization: Bearer sk_xxx或X-API-Key: sk_xxx。 POST /api/keys/validate验证 key + 拿权限。
8. 常见错误
| 错误 | 后果 | 正确 |
|---|---|---|
用 org_id/tenant_id 做用户主键 | 串用户(default 下所有人共享一个 key) | 用 sub |
用 username 做主键 | 改名后身份断裂 | 用 sub(user_id 稳定) |
不校验 aud | 别的业务 token 被你误用 | 验 aud = 你的 client_id |
声明 auth:org scope 但个人场景 | 多此一举(拿到 org_id=default 没用) | 个人场景只声明 openid+profile |
| token 当永久 session | 权限变更后不生效 | token 有 exp,过期 refresh;或用 introspect 实时查 |
| 未定义登出语义 | 用户以为已退出,却因 SSO session 无感登录 | 明确使用本地登出或全局登出;后者跳 end_session,见 3.4 |
9. 可观测性(接入必做,便于跨服务排查)
OIDC 登录链路横跨两个系统:下游业务(发起跳转、收 callback、换 token)↔ AuthKeystone(
/authorize、/oauth/token)。这是登录主链路的核心功能,任一环节断点都需要两边日志对照才能定位。下游接入时必须在自己的三个环节打日志——否则出问题(典型如“登录后没跳回业务“)时只能看到一半,无从定位。
为什么必做
典型故障「用户登录后没回到业务应用」可能断在:
- AuthKeystone 没发 302(
redirect_uri不在白名单 / 用户无登录态 / client 停用) - AuthKeystone 发了 302,但业务 callback 处理出错
- 业务拿到 code 但换 token 失败(code 过期 / PKCE 错 / secret 错 /
redirect_uri不一致)
只看一边无法定位。AuthKeystone 侧已在 /authorize、/oauth/token 关键节点打了日志(含授权码前缀、redirect_uri、错误原因),下游必须在自己这边打对应日志,两边对照。
下游必须记录的三个环节
① 发起 /authorize 跳转前——确认“确实发起了跳转、参数对“:
- 记
client_id/redirect_uri/scope/state(自己生成的)/prompt/ 是否带code_challenge - 级别:info
② 收到 AuthKeystone 回调(你的 redirect_uri 被调用)——判断“登录是否成功“的关键:
- 拿到
code→ info(记 code 前 8 位用于和 auth 串联,见下) state校验通过(防 CSRF)→ info;不通过 → warn 并拒绝- 回调带
error(如login_required/access_denied/invalid_request)→ warn,记error+error_description。这正是“没登录成功“的直接信号,务必记
③ 用 code 换 token(POST /oauth/token):
- 请求发出:info(
grant_type、client_id) - 成功:info(拿到 token,记
sub) - 失败:warn/error,记 HTTP 状态码 + 响应体的
error+error_description。常见:invalid_grant(code 过期/已用)、invalid_client(secret 错)、invalid_request
跨服务串联(报障定位用)
AuthKeystone 日志和下游日志要能对上。两种方式(推荐都做):
- 授权码前缀(最直接):AuthKeystone 签发 code 时记
code_prefix(前 8 位)。下游在 ② 收到 code、③ 换 token 时也记 code 前 8 位。两边按这 8 位一搜即关联,无需任何额外协议。 X-Request-ID:AuthKeystone 对每个响应回传X-Request-ID响应头。下游记下它;报障时把它给 AuthKeystone 运维,用它精确捞对应请求的全部日志。
错误对照表(出问题时怎么读)
| 现象 | 看下游哪条日志 | 看 AuthKeystone 哪条日志 | 结论 |
|---|---|---|---|
| 没跳回业务 | ① 有没有发起跳转 | 有无 /authorize 入口日志 | 下游没发起 / 请求没到 auth(DNS、办公网拦截) |
回调收到 error | ② error=? | /authorize 的 warn(redirect_uri 不在白名单 / 跳登录页) | redirect_uri 配错 / 用户无登录态 |
| 回调拿到 code 但仍没登录成功 | ③ 换 token 失败的 error | /oauth/token 的 warn(invalid_grant 等) | code 过期 / PKCE / secret / redirect_uri 不一致 |
| 两边都成功但用户仍未登录 | ③ 成功拿到 token | /oauth/token 成功 | 下游 session 签发 / 验 token 逻辑问题 |
脱敏
- ✅ 记:
client_id/redirect_uri/scope/state/sub/ code 前 8 位 /error/error_description/X-Request-ID - ❌ 切勿记完整值:
access_token/id_token/refresh_token/client_secret
10. 联调检查清单
下游接入联调时逐项确认:
- OAuth client 注册(client_id/secret/redirect_uri/grant_types)
- scope 声明(勾选你需要的)
- 授权码流程跑通(/authorize → callback → /oauth/token)
- token 验签(JWKS 本地 + aud 校验)
- sub 读法正确(= user_id 字符串,做用户主键)
- /userinfo 调通(按 scope 返 claim)
- refresh_token 续期
- 登录时保存 id_token(供 logout 作 id_token_hint)
- 产品已选择登出语义:仅清当前业务 session,或走
/oidc/end_session退出整个 SSO 会话(见 3.4) - 走 SLO 时:
post_logout_redirect_uri已在 OAuth client「登出回跳白名单」注册(精确匹配,注意尾斜杠),登出后验证确实回跳业务地址 - 可观测性(必做):发起 /authorize、收 callback、换 token 三环节打日志;记录 code 前 8 位 + auth 回传的
X-Request-ID便于跨服务排查(见第 9 节)
11. Agent / MCP 接入(AuthKeystone 作为 MCP server 的授权服务器)
AuthKeystone 是 MCP-ready 的 OAuth 2.1 授权服务器。MCP 规范(2025-06-18 stable)规定 MCP server = OAuth 2.1 Resource Server,需要一个授权服务器发/验 token——AuthKeystone 直接充当这个角色,MCP server 无需自建身份层。
11.1 MCP server / 客户端怎么接入
- 发现授权服务器:MCP 客户端按 RFC 8414 拉
https://auth.ai-as.cc/.well-known/oauth-authorization-server(与 OIDC discovery 内容一致),拿 issuer / authorization_endpoint / token_endpoint / jwks_uri / grant_types_supported / code_challenge_methods_supported。 - 用户登录(人通过 MCP 客户端操作):授权码 + PKCE S256(
scope=openid auth:permissions auth:org),本地 JWKS 验签,校验aud = 你的 client_id。sub做用户主键。 - 服务身份(Agent 平台控制面调 AuthKeystone 的 introspect/revoke/管理 API):用独立 M2M client(仅
client_credentials,无 redirect_uri),token 的aud/sub= client_id、TTL 15 分钟、带jti。permissions按创建路径双口径:管理端(org/system 级)来自命名空间白名单(platform:/capability:/agent:);用户自助(user 级)来自创建者权限委托(service_permissions⊆ 创建者自身权限,见 全平台客户端 3.3 节)。 - 操作授权:
permissionsclaim(module:entity:action,如platform:run:create)作为细粒度判定的输入。授权决策(capability gateway)在 Agent 平台侧,AuthKeystone 只下发权限标识。 - token 安全:refresh token 实行轮换 + 重用检测(OAuth 2.1 §4.3.1,见第 5 节末尾「refresh token 轮换与重用检测」),旧 refresh 重用会吊销整条链——客户端拿到新 refresh 立即替换,勿并发用同一 refresh 发两次。
11.2 Agent 能力全景
| 能力 | AuthKeystone 提供 | 文档 |
|---|---|---|
| 资源/工具级授权 + 实时决策 | POST /api/authorize(关系式授权,撤销立即生效,决策审计可检索) | Agent 资源授权 |
| 身份委托链 | RFC 8693 Token Exchange / OBO(act + agent_id claims,人 → Agent → 工具全链归因) | Token Exchange 与 Agent 身份 |
| Agent 一等身份 | client 绑定 agent_id,client_credentials token 携带;停用即失效 | Token Exchange 与 Agent 身份 |
| 设备流 | RFC 8628(CLI/桌面 Agent 登录) | 设备授权流 |
| resource indicators | RFC 8707(授权/token 请求带 resource → aud 绑定) | 本文 3.3 与 11.1 |
| 动态注册 | RFC 7591/7592(/oauth/register) | 本文 3.7 |
| sub / org_id 透传 | sub = 用户主键;org_id ↔ tenant 映射由平台定义 | 本文第 2 节 |
运行时隔离(Vault/KMS、Sandbox)与 collection→文档包含关系的计算归 Agent 平台。
11.3 resource indicators(RFC 8707)
授权请求带 resource 参数(绝对 URI,可重复):
GET /authorize?...&resource=https%3A%2F%2Fmcp.erp%2Fapi
- client 配置了 resource 白名单时必须子集命中;未配置则接受任意合法绝对 URI
- 换 token 后 access token 的
aud= resource(id_token 的aud恒为 client_id) - token exchange 与设备流同样支持
resource→ aud 绑定