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

阶段 5-B:Inbound OIDC(企业 SSO)设计

auth 反向当一次 OIDC 客户端(RP),让企业租户的员工用企业自家的 IdP(如企业自建 OIDC、Keycloak、Authentik、Azure AD OIDC、甚至另一个 auth 实例)登录 auth。 区别于 auth 现有的 Outbound OIDC(auth 是提供方,租户的客户端用 auth 登录)。 定稿 2026-07-10。整体路线见 docs/TENANT_PHASE1.md


0. 复用基础

  • GitHub OAuth 用户模式handlers.rsoauth_provider/oauth_id 字段 + 首次登录自动建用户)—— Inbound OIDC 用户映射照搬,只是 provider 换成企业 IdP。
  • auth 自己的 OIDC 提供方逻辑/authorize /token /userinfo /jwks)—— 反过来理解协议,RP 端逻辑对称。
  • reqwest(HTTP)+ jsonwebtoken(验 IdP 签的 id_token,可选)+ Redis(state)—— 手写 RP,不引 openidconnect crate。

1. 流程(Authorization Code + PKCE)

①用户点「企业 SSO 登录」(选租户 acme)
②auth → 302 重定向到企业 IdP 的 authorization_endpoint
    ?client_id=...&redirect_uri=https://auth.ai-as.cc/api/auth/sso/callback
    &response_type=code&scope=openid profile email
    &state=<随机,存Redis>&code_challenge=<PKCE S256>&code_challenge_method=S256
③用户在企业 IdP 登录 + 同意
④IdP → 302 回 https://auth.ai-as.cc/api/auth/sso/callback?code=<code>&state=<state>
⑤auth 校验 state(Redis 取出比对,防 CSRF)+ POST IdP token_endpoint 换 token
    body: grant_type=authorization_code, code, redirect_uri, client_id, code_verifier=<PKCE>
    返 {access_token, id_token}
⑥auth 用 access_token GET IdP userinfo_endpoint → {sub, email, name, ...}
⑦用户映射:oauth_provider = "oidc_inbound:{provider_id}", oauth_id = sub
    首次 → 自动建本地用户(tenant_id=provider 配的租户, must_change_password=false)
    已存在 → 直接登录
⑧auth 签自己的 JWT → 重定向前端 #/oauth-callback?token=...(复用现有前端 callback 流程)

2. 表

CREATE TABLE tenant_oidc_providers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id VARCHAR(50) NOT NULL,              -- 该 IdP 属哪个租户(用户登录后归此租户)
    name VARCHAR(100) NOT NULL,                  -- 显示名(如「公司 AD」)
    issuer VARCHAR(255) NOT NULL,                -- IdP issuer(discovery 从 {issuer}/.well-known/openid-configuration 拿 endpoints)
    client_id VARCHAR(100) NOT NULL,
    client_secret VARCHAR(255),                  -- 机密 client 的 secret(公开 client 留空)
    scope VARCHAR(200) DEFAULT 'openid profile email',
    is_active BOOLEAN DEFAULT TRUE,
    created_at DATETIME DEFAULT (datetime('now','+8 hours')),
    updated_at DATETIME DEFAULT (datetime('now','+8 hours'))
);
CREATE INDEX idx_oidc_providers_tenant ON tenant_oidc_providers(tenant_id);

3. IdP 配置:discovery 优先 + 手填兜底

  • 默认:从 {issuer}/.well-known/openid-configurationauthorization_endpoint/token_endpoint/userinfo_endpoint/jwks_uri,缓存(Moka,1h)。
  • 兜底:表加可选字段 authorization_endpoint_override 等,IdP 不支持 discovery 时手填。

4. API

方法路径权限说明
GET/api/auth/sso/:provider_id/start公开生成 state+PKCE 存 Redis,302 重定向到 IdP /authorize
GET/api/auth/sso/callback?code=&state=公开校验 state → 换 token → userinfo → 映射/建用户 → 签 auth token → 302 前端 callback
GET/api/tenant/oidc-providers租户管理员列本租户的 IdP 配置
POST/PUT/DELETE/api/tenant/oidc-providers/:id租户管理员CRUD(租户管理员只能管自己租户的)
GET/api/admin/tenants/:id/oidc-providers平台 admin跨租户查看(运维)

/api/auth/sso/* 是公开入口(未登录),跟 /api/auth/login 同级。


5. 用户映射(复用 GitHub OAuth 模式)

  • oauth_provider = format!("oidc_inbound:{}", provider_id)
  • oauth_id = userinfo.sub(IdP 内唯一,最稳;不用 email 防 IdP 允许改邮箱)
  • 首次登录:INSERT users (username=email 或 name, password_hash=随机不可用, oauth_provider, oauth_id, tenant_id=provider.tenant_id, must_change_password=false)
  • 后续:SELECT ... WHERE oauth_provider=? AND oauth_id=? 命中即登录。
  • 登录后签的 token 含 tenant_id,中间件按租户隔离(阶段1 链路自动生效)。

6. 前端

  • 登录页加「企业 SSO 登录」入口:
    • 起步:输入租户 id(如 acme)→ 列该租户的 active IdP → 点其中一个跳 /api/auth/sso/:id/start
    • 后续:租户子域名(acme.auth.ai-as.cc)自动定租户(要 DNS/证书,晚一步)。
  • 租户管理页加「SSO 配置」tab(或独立页):租户管理员 CRUD IdP(name/issuer/client_id/secret/scope),平台 admin 也能看。
  • callback 复用现有 callback.vue(hash 带 token → applyLogin)。

7. 安全

  • state:随机 + Redis 存(key sso:state:{state}{provider_id, code_verifier, redirect_to},TTL 10min),callback 比对 + 一次性删除(防重放)。
  • PKCE:code_verifier 随机 + code_challenge=S256(随 state 存 Redis),换 token 时带 code_verifier(防 code 拦截)。
  • IdP token 不出后端:access_token/id_token 仅 auth 后端用,不返前端。
  • userinfo 可选验签:拿 JWKS 验 id_token 签名(防伪造),起步可先只信 HTTPS+userinfo(够用),后续加严。

8. 工作量(1 人估)

子项工作量
tenant_oidc_providers 表 + CRUD API + 权限~1.5 天
RP 逻辑(start/callback/换 token/userinfo/state/PKCE/discovery 缓存)~3-4 天
用户映射(复用 GitHub OAuth 模式)~1 天
路由 + 前端 callback 衔接~0.5 天
前端(登录页 SSO 入口 + 租户 IdP 配置页)~2-3 天
测试(Keycloak 或另一个 auth 实例当 IdP)~1-2 天
合计~1.5-2 周

难点:OIDC 协议细节(PKCE 正确性、discovery 解析、token 交换错误处理)+ 真实 IdP 集成测试。


9. 推荐决策(起步,可调)

  1. discovery 优先(填 issuer 自动拿 endpoints),手填兜底。
  2. 登录入口:起步用「输入租户 id 选 IdP」,子域名后续。
  3. 用户创建:首次 SSO 自动建(免管理员预建)。
  4. 手写 RP(复用 reqwest + jsonwebtoken,不加 openidconnect crate)。
  5. 起步可先不验 id_token 签名(信 HTTPS + userinfo),后续加 JWKS 严验。

10. 不在本阶段做

  • ❌ SAML(AD/Okta/Azure 的 SAML 协议,最难,等大客户要再做,~2.5 周)。
  • ❌ 租户子域名 + 自动定租户(要 DNS/通配证书)。
  • ❌ IdP → auth 的用户/组同步(SCIM,企业大客户功能)。
  • ❌ 多 IdP per 租户的复杂 UI(表支持,UI 起步单 IdP 即可)。