Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AuthKeystone IDaaS 下游接入指南

AuthKeystone 是 OIDC 身份提供方(IDaaS),下游业务服务(ExoMind/wechat/…)通过标准 OIDC 接入。 本文档是下游接入的契约与规范——照着做,身份层零踩坑。定稿 2026-07-11。


1. 10 分钟快速接入

按以下顺序完成一次最小可用的 Web 接入。生产环境优先使用标准 OIDC SDK;它会通过发现文档读取端点,避免硬编码。

  1. 在后台创建 OAuth client,登记精确的 redirect_uri;服务端 Web 应用保存 client_secret,SPA 使用 PKCE。
  2. 按需启用 openid profile email 等 scope。需要邮箱不代表邮箱已验证,见第 7 节。
  3. https://auth.ai-as.cc/.well-known/openid-configuration 发现端点,跳转授权端点并生成、保存 state 和 PKCE 参数。
  4. 回调时先校验 state,再用授权码换取 token;回调中的 error 也必须处理并展示给用户。
  5. 用 JWKS 验签 token,并校验 issaud(必须等于自己的 client_id)、有效期和 nonce(使用时)。
  6. sub 建立或查找下游用户,创建自己的业务 session;不要把 token 当作永久 session。

最小流程:

浏览器 -> /authorize -> AuthKeystone 登录 -> 业务 callback
业务后端 -> /oauth/token -> 验签与校验 aud -> 业务 session

2. 四层身份模型

AuthKeystone 的身份架构是四层正交,下游接入前必须分清:

字段语义下游怎么用
身份(Who)sub用户唯一稳定标识(= user_id,跨改名/换组织不变)用户主键:个人知识库、配额、偏好、会话——所有「按用户隔离」的场景都用它
组织(Whose)org_id组织容器(个人/团队/企业)仅团队/企业数据隔离;个人场景不需要。绝不用 org_id 当用户身份
权限(What)permissionsRBAC 权限列表授权判断(或调 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所有
profilepreferred_username, name要展示用户名的(picture 头像字段规划中,暂不签发)
emailemail, email_verified要邮箱的
auth:permissionspermissions[]要做 RBAC 授权的
auth:orgorg_id要做团队/企业隔离的(个人场景不要
业务自定义(如 exomind:wiki业务约定特定业务,向 SaaS 申请

不声明的 scope → token 里没有对应 claim(最小暴露)。例如只要 openid + profile → token 只有 sub + preferred_username,拿不到 org_id/email/permissions

email scope 不等于邮箱已经验证。下游将邮箱用于恢复、授权或账户关联前,必须检查 email_verified=truesub 才是唯一稳定主键。

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 刷新会话),给 /authorizeprompt=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,「切换账号」入口)

业务侧用户已登录,但想提供切换账号能力(头像菜单里「切换账号」按钮、多账号工作台等),给 /authorizeprompt=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),其余与普通登录完全一致,无需新回调逻辑。

用户在业务侧(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_session cookie 残留在浏览器
  • 用户再点登录 → /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.cnhttps://youhuale.cn/);未注册 / 不匹配时 auth 不回跳,302 落 auth 首页并记 SLO_REDIRECT_REJECTED 审计
  • 必须 window.location 浏览器跳转,不能 fetch——跨域请求不带 auth.ai-as.cc 的 cookie,清不掉 auth_session
  • 标准库可自动:发现文档已声明 end_session_endpoint,标准 OIDC SDK(Node openid-client / Python authlib / Java pac4j)发现后会自动在 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 不改代码)。

前端 SPAoidc-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

  • 标准openidsub)/ profilepreferred_username, name)/ emailemail, email_verified
  • AuthKeystone 扩展auth:permissionspermissions[],做 RBAC)/ auth:orgorg_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/introspectPOST /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_idclient_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-configurationOIDC 发现文档(所有端点 + 支持的 scope/claim/grant)
GET /.well-known/oauth-authorization-serverRFC 8414 AS Metadata(纯 OAuth2 / MCP 接入方发现 AS,内容与 OIDC 发现一致)
GET /.well-known/jwks.jsonRS256 验签公钥(本地验 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/authorizeAgent 资源授权实时决策(见「Agent 资源授权」)
GET /api/userinfo用户信息(Bearer token → 标准 OIDC claims)
POST /oauth/introspecttoken 内省(实时查有效性 + 权限)
POST /oauth/revoketoken 吊销
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 校验三步:

  1. 验签 claims 结构加 aud 字段(access_token 可选、id_token 必填,统一用 Option<String> 接收即可)。
  2. 期望值设为你自己的 client_id
  3. 验签时打开 validate_aud = true

/oauth/token(RP 正常接入的入口)拿到的 token,aud 必为你的 client_id

⚠️ 第一方用户 token(aud = issuer)——下游验签必须纳入白名单(2026-09-04 修正)

auth 的用户直登端点(网页登录、微信小程序登录、短信登录等)签发的 access_token,aud 固定为 issuerhttps://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_idaud == client_id
第一方用户会话 token(网页/小程序/短信登录)auth 登录端点issuer白名单 {client_id, issuer}
委托 token(OBO)token exchangeresource 指示器按需

扩展层(按 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 拿不拿得到,分两层看

  1. 「discovery 有」≠「你的 token 有」scopes_supported/claims_supported 声明的是 AuthKeystone 的全局能力,某 client 实际拿得到哪些取决于后台给该 client 勾选的 scope(按「client 启用 ∩ 请求」投影,没勾的 scope 对应 claim 静默不发、不报错)。拿不到先查后台 scope 勾选。
  2. 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. 关键规范

  1. sub 做用户主键——这是 OIDC 标准,跨组织/改名/换租户都稳定。知识库、配额、会话、偏好,全用它。
  2. org_id 仅用于团队/企业隔离——做团队空间、企业数据隔离。个人场景(每个用户独立空间)用 sub,不要 org_id
  3. 验 token 时校验 aud = 你的 client_id——防止别的业务的 token 被你误用。
  4. 只声明你需要的 scope——最小暴露,AuthKeystone 会追踪谁用了哪些字段(迭代时评估影响)。
  5. 业务自定义 scope 走申请制——向 auth SaaS 管理员申请(如 exomind:wiki),不要擅自用未注册的 scope。
  6. 按产品语义选择登出方式——本地登出清业务 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_xxxX-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_typeclient_id
  • 成功:info(拿到 token,记 sub
  • 失败:warn/error,记 HTTP 状态码 + 响应体的 error + error_description。常见:invalid_grant(code 过期/已用)、invalid_client(secret 错)、invalid_request

跨服务串联(报障定位用)

AuthKeystone 日志和下游日志要能对上。两种方式(推荐都做):

  1. 授权码前缀(最直接):AuthKeystone 签发 code 时记 code_prefix(前 8 位)。下游在 ② 收到 code、③ 换 token 时也记 code 前 8 位。两边按这 8 位一搜即关联,无需任何额外协议。
  2. X-Request-ID:AuthKeystone 对每个响应回传 X-Request-ID 响应头。下游记下它;报障时把它给 AuthKeystone 运维,用它精确捞对应请求的全部日志。

错误对照表(出问题时怎么读)

现象看下游哪条日志看 AuthKeystone 哪条日志结论
没跳回业务① 有没有发起跳转有无 /authorize 入口日志下游没发起 / 请求没到 auth(DNS、办公网拦截)
回调收到 errorerror=?/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 / 客户端怎么接入

  1. 发现授权服务器: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。
  2. 用户登录(人通过 MCP 客户端操作):授权码 + PKCE S256(scope=openid auth:permissions auth:org),本地 JWKS 验签,校验 aud = 你的 client_idsub 做用户主键。
  3. 服务身份(Agent 平台控制面调 AuthKeystone 的 introspect/revoke/管理 API):用独立 M2M client(仅 client_credentials,无 redirect_uri),token 的 aud/sub = client_id、TTL 15 分钟、带 jtipermissions 按创建路径双口径:管理端(org/system 级)来自命名空间白名单(platform: / capability: / agent:);用户自助(user 级)来自创建者权限委托(service_permissions ⊆ 创建者自身权限,见 全平台客户端 3.3 节)。
  4. 操作授权permissions claim(module:entity:action,如 platform:run:create)作为细粒度判定的输入。授权决策(capability gateway)在 Agent 平台侧,AuthKeystone 只下发权限标识。
  5. 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 indicatorsRFC 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 绑定