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

统一 IdP 抽象(OIDC + OAuth2)

把 Inbound OIDC 的 tenant_oidc_providers 扩展成「外部身份提供方」统一抽象,让租户在 UI 上自助配置任意登录源——既支持标准 OIDC IdP(Keycloak/Azure/Google),也支持 OAuth2 提供方(Gitee/GitHub/微信/飞书等非标准 OIDC),并按租户停用。 定稿 2026-07-10。前置:Inbound OIDC(docs/TENANT_PHASE5_INBOUND_OIDC.md)、Gitee 硬编码(commit 1a058c7)。


0. 为什么

当前 auth 有两套登录入口:

  • 硬编码 handler:GitHub、Gitee(每家一个 handler + 全局 client_id/secret,所有租户共享,租户无法停用/自定义)。
  • 配置化 IdP:Inbound OIDC(tenant_oidc_providers,每租户配企业 IdP,可停用)——但只支持标准 OIDC(依赖 discovery + id_token + 标准 userinfo),接不上 Gitee/微信等 OAuth2 平台。

目标是统一成一套配置:租户管理员在 UI 配一个 IdP(选类型 OIDC/OAuth2),填对应参数,就能接入任意登录源,并能停用。这是 IDaaS 的核心能力(Auth0/Casbin 的 social connections 都这么设计)。


1. 数据模型扩展(tenant_oidc_providers 加字段)

ALTER TABLE tenant_oidc_providers ADD COLUMN provider_type VARCHAR(10) DEFAULT 'oidc';  -- 'oidc' | 'oauth2'
-- OAuth2 手填 endpoints(OIDC 用 discovery,这几列留空)
ALTER TABLE tenant_oidc_providers ADD COLUMN authorize_url TEXT;
ALTER TABLE tenant_oidc_providers ADD COLUMN token_url TEXT;
ALTER TABLE tenant_oidc_providers ADD COLUMN userinfo_url TEXT;
-- OAuth2 userinfo 字段映射(OIDC 用标准 sub/email,这几列留空走默认)
ALTER TABLE tenant_oidc_providers ADD COLUMN field_id VARCHAR(50) DEFAULT 'id';
ALTER TABLE tenant_oidc_providers ADD COLUMN field_username VARCHAR(50) DEFAULT 'login';
ALTER TABLE tenant_oidc_providers ADD COLUMN field_email VARCHAR(50) DEFAULT 'email';
ALTER TABLE tenant_oidc_providers ADD COLUMN field_name VARCHAR(50) DEFAULT 'name';
ALTER TABLE tenant_oidc_providers ADD COLUMN field_avatar VARCHAR(50) DEFAULT 'avatar_url';
-- OAuth2 token 传递与编码
ALTER TABLE tenant_oidc_providers ADD COLUMN userinfo_token_in VARCHAR(10) DEFAULT 'query';  -- 'query'(?access_token=) | 'header'(Authorization: Bearer)
ALTER TABLE tenant_oidc_providers ADD COLUMN token_content_type VARCHAR(10) DEFAULT 'form';  -- 'form'(x-www-form-urlencoded) | 'json'

现有 IdP 配置迁移:provider_type='oidc'(默认),新字段空走 OIDC discovery,零破坏。


2. RP 逻辑泛化(src/sso.rs

start(GET /api/auth/sso/:provider_id/start

provider_type 分支:

  • oidcdiscover(issuer) 拿 authorization_endpoint + 生成 PKCE(code_challenge)+ state 存 Redis → 302。(现有逻辑)
  • oauth2:用 authorize_url(手填)+ state 存 Redis(不生成 PKCE,多数 OAuth2 如 Gitee/GitHub 不要求;若某些平台要,可后续加 use_pkce 开关)→ 302。

callback(GET /api/auth/sso/callback?code=&state=

  • 换 token:
    • oidcPOST discovery.token_endpoint + code_verifier(PKCE)→ {access_token, id_token}
    • oauth2POST token_url,body 按 token_content_type(form/json),含 grant_type=authorization_code, code, client_id, redirect_uri, client_secret(无 code_verifier)→ {access_token}
  • userinfo:
    • oidcGET discovery.userinfo_endpoint + Authorization: Bearer → 标准 {sub, email, name}
    • oauth2GET userinfo_url,token 按 userinfo_token_in(query ?access_token= 或 header)→ 按 field_* 映射取 {id, username, email, name, avatar}
  • 用户映射(统一):身份 = (provider_id, provider_kind),登录/绑定走 user_idp_bindings 表(多绑模型,2026-09);users.oauth_provider = "idp:{id}" / oauth_id = mapped_id 两列仅为双轨投影(写时同步)。

公开端点(GET /api/auth/sso/providers?tenant_id=

[{id, name, provider_type}](加 provider_type 供登录页按类型显示图标/文案;不返敏感)。


3. 字段映射示例(覆盖主流 OAuth2 平台)

平台authorize_urltoken_urluserinfo_urlfield_idfield_usernameuserinfo_token_intoken_content_type
Giteehttps://gitee.com/oauth/authorizehttps://gitee.com/oauth/tokenhttps://gitee.com/api/v5/useridloginqueryform
GitHubhttps://github.com/login/oauth/authorizehttps://github.com/login/oauth/access_tokenhttps://api.github.com/useridloginheaderjson
通用 OAuth2各自填各自填各自填配置配置配置配置

scope 按平台官方词表填(预置:GitHub read:user user:email、Gitee user_info——Gitee 合法 scope 不含 emails,塞了会 invalid_scope 登录断)。私有邮箱补全不靠 scope,靠 emails_url(邮箱补全端点):userinfo 未返回 email 时回退拉取该端点,只取已验证条目(GitHub verified=true / Gitee state=confirmed,primary 优先);预置 GitHub .../user/emails、Gitee /api/v5/emails

租户管理员在 UI 填这些 → 接入对应平台。


4. UI(admin-ui/src/sso_providers.vue

  • 表单加「类型」选择:OIDC / OAuth2。
  • OIDC 显示:issuer(discovery 用)。
  • OAuth2 显示:authorize_url / token_url / userinfo_url + 字段映射(field_id/username/email/name/avatar)+ userinfo_token_in(query/header)+ token_content_type(form/json)+ scope + emails_url(邮箱补全端点,可空——userinfo 无 email 时回退拉取,只取已验证条目)。
  • 通用:name / client_id / client_secret / is_active(停用开关)。
  • 预置模板(可选 UX 加分):点「Gitee 模板」自动填 Gitee 的 URL/字段映射,租户只补 client_id/secret。

5. 硬编码 GitHub/Gitee 已退役(2026-07)

  • 硬编码 github_oauth_* / gitee_oauth_* handler 与 .envGITHUB_* / GITEE_* 配置已移除
  • 全部统一到配置化 IdP:社交登录(GitHub/Gitee)和企业 SSO(OIDC)都走 tenant_oidc_providers 表 + /api/auth/sso/* 路径。
  • 功能平移:db.rs 启动迁移在 default 租户下自动创建 GitHub OAuth2 IdP(读 .envGITHUB_CLIENT_ID/SECRET,幂等判重);Gitee 早已预置(id=1)。
  • 历史 oauth_provider 值:硬编码 'github'/'gitee'(遗留账号)vs 配置 'idp:{id}'(新登录)。两套不冲突,老账号原值不变。2026-09 多绑模型迁移(schema v7)已把两套存量回填进 user_idp_bindings:能映射到 provider 记录的按 (provider_id, 'oauth2') 归一,映射不到的落 legacy 负哨兵;users 两列此后由写路径同步维护(投影)。

6. 工作量(1 人估)

子项工作量
表扩展 + 迁移 + 模型字段~1 天
RP 逻辑泛化(start/callback 按 type 分支 + 字段映射 + token 传递)~3 天
公开端点 + UI(sso_providers.vue type 选择 + 条件字段 + 模板)~2 天
测试(oidc 路径不回归 + oauth2 用 Gitee 实测)~1 天
合计~1 周

7. 微信小程序登录(2026-09 上线,wechat.rs接入方请看 WECHAT_MINIPROGRAM 契约文档

微信是自有协议(非标准 OAuth2/OIDC,无 redirect 授权流),不走上文的 provider 通用流程,独立成模块:

  • 配置:org_idp_providers 一行 provider_type='wechat'——client_id=小程序 AppID、client_secret=AppSecret(AES 加密落库);不进网页登录按钮(公开列表端点过滤 wechat 类型)。
  • 身份键 = unionid 优先subject = unionid(拿不到则 openid),bindings 按 (provider_kind='wechat', subject) 匹配——「微信里的这个人」跨小程序/公众号是同一账号(provider_id 仅记录来源小程序;schema v9 部分唯一索引保证一人一行)。
  • 三端点(公开):
    • POST /api/auth/wechat/login {code, appid}——code2session 换身份;已绑定直接签 JWT;未绑定不建号,返 {status:"unbound", wx_token}(Redis 一次性 5min 票据)。
    • POST /api/auth/wechat/register {wx_token}——快速开通:建无邮箱无密码的微信专属账号(后续可转正绑邮箱)。
    • POST /api/auth/wechat/bind {wx_token, username, password, mfa_code?}——绑老号:凭证验证(login_guard 双维度限流 + TOTP 老号必须带码)→ link_identity → 签老号 token。凭证失败不烧票据可重试。无密码老号(Gitee 建号)明确报错引导(设置密码或改用确认码)。
    • 确认码绑定(免输密码,2026-09):小程序 POST /api/auth/wechat/bind-code {wx_token} 换 6 位码(5 分钟,错 5 次作废)→ 用户在已登录的网页端个人中心「关联小程序」输码 POST /api/auth/wechat/confirm-bind {code}(老号登录态即控制权证明,无密码老号同样适用)→ 小程序 POST /api/auth/wechat/poll-bind {code} 轮询取老号 token 对(票据一次性)。
  • 首登不建号的理由:微信无邮箱 → 双侧已验证邮箱合并天然失效,即建号必产生「老用户扫码进空号」;二选一把选择权给用户,不产生双号。
  • 安全红线:session_key 拿到即弃(不存不下发);code 一次性;AppSecret 只在服务端。
  • 边界:身份归 auth(直调 jscode2session,与 GitHub/Gitee IdP 同构);wechat-publish-service 等下游应用保持自身用户体系不动,可渐进切换。本地 mock:WECHAT_API_BASE 覆盖 API 域名。

7b. QQ 互联(2026-09-05 上线,provider_type=‘qq’)

redirect 流的「微信式特例」——协议形态介于标准 OAuth2 与自有协议之间,callback 走专属分支:

  • 配置:管理台「组织 IdP 配置」类型选 QQ 互联——client_id=APP ID、client_secret=APP Key(AES 加密落库);四端点内置免填(QQ_API_BASE 可覆盖供 mock)。QQ 后台的回调地址填 https://auth.ai-as.cc/api/auth/sso/callback
  • 协议特例三处(callback 专属分支):token 端点 GET、响应是 form 文本(access_token=...&expires_in=...);身份键 openid 由独立 me 端点返回(JSONP 剥壳,申请了 unionid 机制的应用带 unionid);userinfo 需附 oauth_consumer_key(APPID)+openid、无 email(JIT 建号无邮箱,用户后续自助绑邮箱)。
  • 身份家族kind='qq',subject = unionid(有则用)或 openid;登录/属主查询按 (kind, subject)(对称微信家族)——同一开发者账号下多 QQ 应用 unionid 同号(schema v11 部分唯一索引)。首个应用未申请 unionid 机制时 subject=openid,将来申请后新登录走 unionid(老绑定不动,用户重绑即迁移)。
  • 登录页:QQ 按钮出现在网页登录页/绑定卡(与 Gitee 同列,redirect 流)。

8. 边界(本阶段不做)

  • ❌ 硬编码 GitHub/Gitee 迁移到 IdP 配置(保留兼容,迁移影响现有用户 oauth_provider 值)。
  • 微信/飞书自有协议适配(微信小程序已于 2026-09 落地,见 §7;飞书仍不做)。
  • ❌ SAML(仍等大客户要再做)。
  • ❌ 租户级硬编码停用(硬编码是平台预置全局,租户不能停;租户只管自己配的 IdP)。