oauth: bind existing accounts to external identities

New external_identities table (UNIQUE(provider, external_id),
UNIQUE(provider, user_id)) + bind/initiate, bindings list/unbind
endpoints. Callback branches on bind_user_id in state: bind success
shows no key and no user id; conflicts are 409
(identity_bound_to_other / user_already_bound); repeat bind to the
same user+identity is idempotent. Login resolution checks bindings
first: a bound identity lands in the bound account and rotates its
key like any OAuth login; unbound identities keep the deterministic
oauth:<provider>:<login> mapping. Unbind restores oauth mapping.
Tests: new cloud/tests/test_oauth_bind.py (11). Full suites: host
29 OK, cloud 389 OK (4 pre-existing skips).
This commit is contained in:
host committed 2026-10-26 15:47:25 +08:00
1 parent 5735ea16d6
commit f12b18412a
4 files changed
+675 -28

No files matched your search

+19 -2
View File
@@ -139,8 +139,25 @@ client_secret(只走环境变量),不是无认证后门。
已存在用户每次 OAuth 登录都会轮换 key(旧 key 立即失效),
因此不要把旧 key 存到多处长期复用;key 明文只在回调成功页/
响应里出现一次,丢了只能重新走一次 OAuth 登录拿新 key。
- 仍缺(非部署前置,但同样未完成):已有本站账号的绑定流程
(目前是确定性用户名新建/复用,不做账号绑定)。
- 已有本站账号的绑定流程(已实现):持本站 key 调
`POST /oauth/{provider}/bind/initiate`(body 含 client_id +
redirect_uri),返回 `{provider, state, authorization_url}`,
state 里带 bind_user_id(10 分钟过期、一次性);用户授权后
回调走绑定分支(GET 回调成功页只显示 provider + 外部 login,
绝不显示任何 key、也不显示本站 user_id;POST 回调返回
`{linked: true, provider, login, external_id, user_id,
already_bound}`,同样不含 key)。绑定表
`external_identities` 有两个唯一约束:一个外部身份只能绑一个
本站账号、一个本站账号每个 provider 只能绑一个外部身份,
冲突返回 409(`identity_bound_to_other` /
`user_already_bound`),同一用户 + 同一身份重复绑是幂等的。
登录解析顺序:先查绑定表——绑过的身份落到被绑账号并按 OAuth
登录规矩轮换 key;没绑过才走确定性 oauth 账号映射。以前
GitHub 登录建的 oauth 账号不会被绑定删掉:绑完后该身份登录
改落到被绑账号,旧 oauth 账号成孤儿(key 仍可用);解绑
(`DELETE /oauth/{provider}/bindings`,须持本站 key,本来就
没有返回 404)后恢复原来的 oauth 账号登录。查看自己的绑定:
`GET /oauth/{provider}/bindings`(须持本站 key,只看得到自己的)。
### 推送凭证清单