OAuth real browser login: GET callback, userinfo, API key issuance

Add GET /oauth/{provider}/callback (code+state from query, client_id
and redirect_uri recovered from the state record) alongside the
existing POST callback. Callback now fetches real userinfo
(GitHub api.github.com/user, Google openidconnect userinfo) and
issues a usable local API key on success: new users get the
create_user key, existing users rotate (old key dead, audit
oauth_login + api_key_rotated). Key plaintext appears only once
on the success page/response; failures are 400 HTML/JSON without
secrets, and a missing client_secret fails before any network call.
State remains one-time, 10 minutes, provider/redirect_uri matched.
Tests: 10 new OAuth real-login tests pass; host 27, cloud 249
(4 skipped are the pre-existing real-Postgres tests).
Real end-to-end still needs a registered OAuth App and a public
HTTPS callback entry (server still binds localhost only).
This commit is contained in:
host committed 2026-10-26 03:35:40 +08:00
1 parent fbb00b8b6f
commit 0ed301b010
5 files changed
+637 -69

No files matched your search

+59 -19
View File
@@ -83,19 +83,56 @@ X-API-Key,因为外部服务无法持本站 API key,改靠各自的签名认
本站 API key——它的认证靠随机 state(10 分钟过期、一次性)加
client_secret(只走环境变量),不是无认证后门。
### OAuth 接线步骤(脚手架,已实现,未接真实应用)
### OAuth 真实浏览器登录(已实现,部署前置仍需用户动手)
1. 注册真实 OAuth 应用(GitHub 或 Google),拿到 client_id,
回调地址填本站的 `/oauth/{provider}/callback` 公网地址。
2. `GET /oauth/{provider}/login?client_id=...&redirect_uri=...` 拿
authorization_url,把用户送到 provider 授权页。
3. provider 回调本站并带回 code + state,调
`POST /oauth/{provider}/callback`,验 state → 换 token → 映射
本站用户并记审计。client_secret 只从环境变量读
(`GITHUB_OAUTH_CLIENT_SECRET` / `GOOGLE_OAUTH_CLIENT_SECRET`),
绝不写文件、绝不进 URL、换 token 前未配好就明确报错。
4. 仍缺:真实应用注册、回调公网地址、已有本站账号的绑定流程
(目前是确定性用户名新建/复用,不做账号绑定)。
流程(标准 OAuth2 授权码流程):
1. `GET /oauth/{provider}/login?client_id=...&redirect_uri=...`
返回 `{provider, state, authorization_url}`;state 已入库
(10 分钟过期、一次性,provider / redirect_uri / client_id
都随 state 一起存下),把用户送到 provider 授权页。
2. 用户授权后 provider 重定向到本站
`GET /oauth/{provider}/callback?code=...&state=...`(真实浏览器
流程只带 code + state;client_id / redirect_uri 由 server 从
state 记录取回,不需要 provider 额外带回)。
3. server 验 state → 用 code 换 token → 调真实 userinfo 取身份
(GitHub:`https://api.github.com/user`,Bearer + User-Agent,
取 login/id;Google:`https://openidconnect.googleapis.com/v1/userinfo`,
取 sub/email/name)→ 映射到本站用户(确定性用户名
`oauth:<provider>:<login>`,新建或复用)→ 签发本站 API key
并记审计 `oauth_login`(轮换时另记 `api_key_rotated`)。
4. 成功页(GET 回调)只显示 login、user_id、是否新用户和新 API
key 一次,并写清“请立即保存,刷新后不再显示,旧 key 已失效
(如系轮换)”;成功页绝不显示 access_token、client_secret、
旧 API key。`POST /oauth/{provider}/callback`(JSON 版,body 带
code/state,client_id/redirect_uri 可省、从 state 取回)同样在
成功响应里返回一次新 API key;之后任何接口都不再返回它。
失败一律 400:state 无效/过期/不匹配、换 token 失败、userinfo
失败、client_secret 缺失都有明确 reason,且不建用户、不发 key。
state 仍是一次性、10 分钟过期、provider / redirect_uri 必须匹配。
真实部署前置(以下任何一项没做,都不算真登录,绝不是“改个
环境变量就自动有公网登录”):
- 在 GitHub 上注册 OAuth App(或 Google 上注册 OAuth 客户端):
应用名自定;Homepage URL 填本站公网地址;Authorization
callback URL 必须与本站回调地址逐字一致,即
`https://<你的域名>/oauth/github/callback`(Google 同理);
非 localhost 时 GitHub 要求回调地址是 https,http 会被拒绝。
- client_id 由登录请求方传入(login 接口的 query 参数不变);
client_secret 只从环境变量读(GitHub
`GITHUB_OAUTH_CLIENT_SECRET`、Google
`GOOGLE_OAUTH_CLIENT_SECRET`),绝不写文件、绝不硬编码、
绝不出现在授权 URL 或成功页里;secret 缺失时回调明确报
`client_secret_missing`,不会以空 secret 去撞真实端点。
- 本站控制面必须有公网可达的 https 回调地址:目前 server 默认
只绑 `127.0.0.1:8765`,没有公网入口、没有 TLS,这一步是部署
前置——需要在部署侧配反向代理 + TLS 证书并把公网域名指到
server,再把该域名下的回调地址填进 OAuth 应用的注册信息里。
- 登录后 API key 的保存与轮换规则:新用户拿到创建时生成的 key;
已存在用户每次 OAuth 登录都会轮换 key(旧 key 立即失效),
因此不要把旧 key 存到多处长期复用;key 明文只在回调成功页/
响应里出现一次,丢了只能重新走一次 OAuth 登录拿新 key。
- 仍缺(非部署前置,但同样未完成):已有本站账号的绑定流程
(目前是确定性用户名新建/复用,不做账号绑定)。
### 推送凭证清单
@@ -245,7 +282,8 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
| `GET /projects/{id}/cost` | 该项目 `read` | 项目成本总览 + 按天:`?day=YYYY-MM-DD` 查单天,`?days=N` 查最近 N 天;无参数只给总览 |
| `GET /models` | 认证即可 | 优先级列表(环境变量 `MODELS_PRIORITY` 逗号分隔,未设用默认)+ 只含调用者成员项目的模型统计与 fallback 事件(`fallback_count > 0` 的任务);不返回 prompt 内容 |
| `GET /oauth/{provider}/login?client_id=&redirect_uri=&scope=` | 无需 X-API-Key(pre-auth) | 返回 `{provider, state, authorization_url}`,只返回 URL 不跳转;provider 仅 github/google,未知 `400`;state 已入库,10 分钟过期 |
| `POST /oauth/{provider}/callback` | 无需 X-API-Key(pre-auth) | body `{code, state, client_id, redirect_uri}`;验 state → 换 token → 映射到本站用户(复用 users 表,用户名 `oauth:<provider>:<login>`)并记审计 `oauth_login`;state 无效/过期、换 token 失败、无 secret 一律 `400` 且不建会话;成功不返回 API key(发放方式由调用方另行决定) |
| `GET /oauth/{provider}/callback?code=&state=` | 无需 X-API-Key(pre-auth) | 真实浏览器回调:验 state(从 state 记录取回 client_id / redirect_uri)→ 换 token → 真实 userinfo 取身份 → 映射本站用户并签发 API key;成功返回 HTML 成功页(login、user_id、是否新用户、新 API key 一次,含保存与轮换提示,不含 access_token / client_secret / 旧 key);state 无效/过期/不匹配、换 token 失败、userinfo 失败、无 secret 一律 `400` 错误页且不建用户、不发 key |
| `POST /oauth/{provider}/callback` | 无需 X-API-Key(pre-auth) | JSON 版回调:body `{code, state, client_id?, redirect_uri?}`(后两者可省、从 state 取回);验 state → 换 token → 真实 userinfo 取身份 → 映射到本站用户(复用 users 表,用户名 `oauth:<provider>:<login>`)并签发 API key、记审计 `oauth_login`(轮换时另记 `api_key_rotated`);state 无效/过期、换 token 失败、userinfo 失败、无 secret 一律 `400` 且不建用户、不发 key;成功响应里返回一次新 API key,之后任何接口都不再返回它 |
| `POST /projects/{id}/push/subscriptions` | 该项目 `create_task` | 记订阅 body `{channel, target, preference?}`,channel 为 webhook/slack/webpush/apns/fcm,preference 为 instant/digest/silent,返回 `201` |
| `GET /projects/{id}/push/subscriptions` | 该项目 `read` | 只列调用者自己的订阅(不泄露他人 target) |
| `DELETE /projects/{id}/push/subscriptions/{sid}` | 该项目 `create_task` | 只能删自己的订阅,项目 owner 可删任意;不存在 `404`,他人订阅 `403` |
@@ -338,12 +376,14 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
Secure Vault 注入、或其它经用户明确同意的渠道),而不是把 key
写进配置文件、数据库或代码里凑合。定下之前,定时自动化与 worker
的模型回合都只能停在“失败路径可用、模型跑不起来”的状态。
- **其它待接线(非外部条件,但同样未完成)**:OAuth 已接成脚手架
端点(state + 授权 URL + 回调),但真实应用注册、回调公网地址、
已有账号绑定仍缺;推送已接成订阅 + 分发端点,但 Web Push /
APNs / FCM 的真实凭证与发送端仍缺;单机 SQLite 在高并发写下
仍有上限,生产量上来后需换 Postgres(Postgres 后端已接通,
但真实服务器部署与验证仍缺,见上文“数据库后端选择”);
- **其它待接线(非外部条件,但同样未完成)**:OAuth 真实浏览器
登录已接通(GET 回调 + 真实 userinfo + 回调签发 API key),但
真实应用注册、回调公网 https 入口、已有账号绑定仍缺(见上文
“OAuth 真实浏览器登录”一节的部署前置);推送已接成订阅 +
分发端点,但 Web Push / APNs / FCM 的真实凭证与发送端仍缺;
单机 SQLite 在高并发写下仍有上限,生产量上来后需换 Postgres
(Postgres 后端已接通,真实服务器部署与验证见上文“数据库后端
选择”);
worker 执行已接通(见下文“worker 常驻与真取消”),云端执行、
真实通知与真实模型回合仍受 key 供给与云账号卡点限制。