阶段 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.rs的oauth_provider/oauth_id字段 + 首次登录自动建用户)—— Inbound OIDC 用户映射照搬,只是 provider 换成企业 IdP。 - auth 自己的 OIDC 提供方逻辑(
/authorize/token/userinfo/jwks)—— 反过来理解协议,RP 端逻辑对称。 reqwest(HTTP)+jsonwebtoken(验 IdP 签的 id_token,可选)+Redis(state)—— 手写 RP,不引openidconnectcrate。
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-configuration拉authorization_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/证书,晚一步)。
- 起步:输入租户 id(如
- 租户管理页加「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. 推荐决策(起步,可调)
- discovery 优先(填 issuer 自动拿 endpoints),手填兜底。
- 登录入口:起步用「输入租户 id 选 IdP」,子域名后续。
- 用户创建:首次 SSO 自动建(免管理员预建)。
- 手写 RP(复用 reqwest + jsonwebtoken,不加 openidconnect crate)。
- 起步可先不验 id_token 签名(信 HTTPS + userinfo),后续加 JWKS 严验。
10. 不在本阶段做
- ❌ SAML(AD/Okta/Azure 的 SAML 协议,最难,等大客户要再做,~2.5 周)。
- ❌ 租户子域名 + 自动定租户(要 DNS/通配证书)。
- ❌ IdP → auth 的用户/组同步(SCIM,企业大客户功能)。
- ❌ 多 IdP per 租户的复杂 UI(表支持,UI 起步单 IdP 即可)。