跳转到内容

OIDC 接入

本文面向在本企业内接入统一身份的应用开发者,说明应用注册与审批、标准 OIDC 参数、 令牌语义、退出联动与常见错误。

yufu 是单企业独立部署的统一身份与 SSO 提供方。一个进程同时提供三个入口:

  • 账户中心:/account/*,本人资料、授权应用、同意、会话与 RP 会话
  • 开发者控制台:/developer/*,本人应用的注册、配置、版本、提交审批与成员
  • 企业管理控制台:/admin/*,账户、应用、组、分配、审批、审计与历史纳管

接入采用企业邀约 / 审批制,不提供公开应用市场。当前明确不做:多租户、外部 IdP 联邦、 公开开发者自助注册、强认证(WebAuthn 等)。接入方不得依赖这些能力。

  1. 管理员为开发者授予开发者资格;未获资格时 /developer/* 返回拒绝。
  2. 开发者在 /developer/apps 注册应用,分别配置 development 与 production 环境。
  3. 生产配置提交后进入审批;管理员在 /admin/apps/:id 审阅固定申请快照并批准。
  4. 管理员在 /admin/apps/:id/assignments 为账户或组分配应用;测试环境可额外授予测试用户访问。
  5. 用户首次登录时在同意页确认请求的 scope;未获分配、未批准或未同意的请求不会签发 code。

开发环境用于联调,生产环境用于真实用户。两环境有独立的 client、回调白名单、access audience、 secret 与配置版本;不要把开发凭据带到生产。

Issuer 为部署的公开基址(生产必须 HTTPS)。Discovery 位于:

GET {issuer}/.well-known/openid-configuration
GET {issuer}/.well-known/jwks.json
端点 路径 说明
Authorization /oidc/authorize 仅 response_type=code、response_mode=query、PKCE S256
Token /oidc/token authorization_code 与 refresh_token
UserInfo /oidc/userinfo Bearer access token
Introspection /oidc/introspect Reader 凭据(client_secret_basic / post)
Revocation /oidc/revoke 支持 access / refresh token
End session /oidc/end-session 仅在 RS256 签名时发布
JWKS /.well-known/jwks.json RS256 公钥

支持的 scope 为 openid、profile、email,其中 openid 必填。签名算法以部署配置为准; 参考示例按 RS256 验证。

  • 服务端持有 client_secret,使用 client_secret_basic 或 client_secret_post 兑换 code。
  • 回调地址必须与注册值逐字符一致,包括 scheme、host、port、path,不允许通配或前缀匹配。
  • 浏览器只接触 issuer、client id、redirect URI、scope;secret 与 Reader 凭据不得进入前端。
  • 不配置 client_secret,使用授权码 + PKCE S256。
  • code 兑换由 SPA 直接完成或由同站 BFF 完成;本仓库示例选择 SPA 直接兑换。
  • 回调页必须是 SPA 的注册回调,不要复用 Web 客户端的回调。
  • 保护 API 只接受注册的 SPA origin。带 Origin 的请求必须精确匹配 scheme/host/port, 不匹配直接 403,且在读取 token 之前拒绝。
  • 预检只允许 authorization、content-type 请求头与 POST, OPTIONS 方法。
  • 中心的管理与账户接口使用同源 Cookie + CSRF,不使用旧 Bearer;跨站调用不应依赖中心 Cookie。
  • sub 是稳定主体标识;同一 (iss, sub) 在 A、B 两个应用间保持一致。应用应把 iss + sub 作为本地用户键,不要用 email 或 username 作为主键。
  • profile scope 披露 name、preferred_username;email scope 披露 email、email_verified。 未授予的 scope 不披露对应字段。
  • ID Token 的 aud 是客户端 id,包含 nonce、auth_time、sid;sid 是协议会话标识, 用于退出联动。
  • Access Token 的 typ 为 at+jwt,aud 是应用环境的 access audience(例如 urn:app:production),不是 client id。保护 API 必须同时校验签名、issuer、audience、 typ 与有效期。
  • 内省响应包含 active、scope、client_id、username、token_type、exp、sub; 不保证返回协议 sid。按会话清理缓存时,同时按 sid 与 sub 清理。
  • Access Token 中的 session_id 是凭据会话 id,不是协议 sid,不要混用。

参考实现共用同一套语义:

  • 正向结果默认缓存 2 秒;超过 2 秒必须重新内省。
  • 任何结果(含层间传递)绝对年龄不得超过 5 秒;超过即失败关闭。
  • 年龄从内省请求开始的单调时刻计算,慢内省不会获得新的 2 秒窗口。
  • 层间传递保留原始 verifiedAt,接收层不得重置 TTL。
  • token 已过期、内省不可用、active=false、client 不匹配、subject 不匹配都拒绝; 拒绝时不执行受保护副作用。

5 秒是硬上限,不是“每 5 秒刷新一次”的授权周期。需要更强实时性的写操作应缩短缓存或直接内省。

  • 本地退出:只清本应用会话,不影响其他应用。
  • 当前根退出:清当前中心浏览器会话及其派生的 RP 会话。
  • 所有根退出:清该用户全部中心根与 RP 会话。
  • RP 发起退出:使用 id_token_hint 与 post_logout_redirect_uri,端点由 discovery 发布。
  • 后通道退出:typ=logout+jwt、RS256、aud=client_id、events 仅含后通道事件、 必须有 jti 与 sid 或 sub、不得含 nonce、exp-iat ≤ 120 秒。接收方必须校验 签名 / issuer / audience / 事件 / 时效,并按 jti 幂等。
  • 把 client id 当作 access token 的 aud:会拒绝所有保护请求,应使用环境注册的 access audience。
  • 在 SPA 中放 client_secret 或 Reader 凭据:会进入浏览器包,属于凭据泄漏。
  • 回调地址使用通配或尾斜杠不一致:授权请求会被拒绝。
  • 只按 sid 清理授权缓存:内省可能没有 sid,必须同时按 sub。
  • 把 ID Token 当 access token 调用 API:typ 不是 at+jwt,必须拒绝。
  • 用旧 Bearer 访问中心管理接口:中心控制台使用同源 Cookie + CSRF。