GitHub / Gitea OAuth 接入¶
QuantumAtlas 用 PocketBase 内嵌 OAuth2 做用户登录,支持两个 provider:GitHub(必需)与 Gitea/Forgejo(可选,自建实例)。这一节讲:怎么建 OAuth App、怎么配 server、多边缘节点的多 App 策略。
两个 provider 的浏览器回调地址相同:SPA 的 /auth/callback 路由(web/src/lib/auth.ts 的手动 code exchange 流程),不是 PocketBase 自带的 /api/oauth2-redirect。
一次性配置(GitHub)¶
1. 在 GitHub 创建 OAuth App¶
去 https://github.com/settings/developers(个人)或 <https://github.com/organizations/
字段 |
填什么 |
|---|---|
Application name |
|
Homepage URL |
|
Authorization callback URL |
|
callback URL 必须精确匹配
server 上启用 OAuth 时只接受配置里那个 callback URL;DNS 转跳、subdomain 切换都会让 OAuth 失败。
创建后会拿到 Client ID(公开)和 Client Secret(机密)—— secret 只显示一次,立即复制。
2. 写到 server config.yaml¶
auth:
github_client_id: Ov23liXXXXXXXXXXXXXX
github_client_secret: ghxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
allowed_logins: [your-gh-login] # 登录白名单,大小写不敏感
admin_logins: [your-gh-login] # 白名单成员 + 运维面 admin
3. 重启 server¶
sudo systemctl restart qatlasd
启动时 internal/auth/oauth.go::Register 会把这两个值注入 PocketBase users collection 的 OAuth2 providers 设置——无需手动在 admin UI 配。
4. 验证¶
# 检查 OAuth provider 已生效
curl https://<your-server>/api/collections/users/auth-methods | jq
# 应该看到 .oauth2.providers[] 里有 "github" 条目(配了 Gitea 则还有 "gitea")
或浏览器打开 https://<your-server>/ → 登录页应该有 "使用 GitHub 登录" 按钮。
一次性配置(Gitea,可选)¶
Gitea provider 面向自建 Gitea/Forgejo 实例(PocketBase 内置的 gitea provider 默认指向 gitea.com,qatlasd 会用 gitea_url 覆盖三个端点 URL)。
1. 在 Gitea 创建 OAuth2 App¶
以实例管理员或普通用户身份打开 <你的 Gitea>/user/settings/applications → 管理 OAuth2 应用程序 → 创建:
字段 |
填什么 |
|---|---|
应用名称 |
|
重定向 URI |
|
创建后拿到 Client ID(UUID 形如 12345678-1234-1234-1234-123456789abc)和 Client Secret(gto_…,只显示一次)。
2. 写到 server config.yaml¶
auth:
# ... github_* 同上 ...
gitea_url: https://git.example.com # 实例 origin,末尾不带 /
gitea_client_id: 12345678-1234-1234-1234-123456789abc
gitea_client_secret: gto_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
gitea_admin_logins: [your-gitea-login]
# 可选:Gitea 侧的 superadmin 播种名单(与 GitHub 的互不相通)
gitea_superadmin_logins: [your-gitea-login]
要点:
gitea_url必填:只配 client id/secret 而漏了它,启动日志会报gitea oauth provider sync failed并跳过该 provider(绝不静默回落到 gitea.com)。Gitea 登录不做白名单:实例上任何账号都可以登录(实例自身的注册/审核策略就是门槛)。
gitea_admin_logins/gitea_superadmin_logins只授予 admin / superadmin,与 GitHub 名单互不相通——同名账号互不继承权限。登录成功后 Gitea 用户名会落到 users 记录的
gitea_login字段(迁移1789000000_add_gitea_login_to_users.go自动加列),/api/me、/api/admin/users均返回该字段。
3. 重启并验证¶
sudo systemctl restart qatlasd
curl https://<your-server>/api/collections/users/auth-methods | jq '.oauth2.providers[].name'
# 期望含 "github" 与 "gitea";登录页出现 "使用 Gitea 登录" 按钮
冲突匹配与账号绑定¶
跨 provider 的"同人不同账号"由两层机制处理(internal/auth/oauth_conflict.go + dashboard 绑定面板):
登录冲突提示:当一个全新 OAuth 身份登录时与现有账号撞车——
邮箱冲突:PocketBase 默认会把同邮箱的新身份静默挂到现有记录上(对未验证邮箱的服务这还是个接管漏洞);qatlasd 会拦下来返回 409,SPA 在回调页弹出"是否匹配已有账号"提示。
用户名冲突:新登录的 GitHub/Gitea 用户名与现有
github_login/gitea_login相同(大小写不敏感),同样弹提示。
提示页的三个选项:
选项 |
行为 |
|---|---|
匹配到已有账号 |
跳到登录页(带引导横幅),用已有账号登录,再在控制台完成绑定 |
继续,使用独立账号(仅用户名冲突) |
重新走一次授权跳转(服务端已记录 15 分钟内的"已提示"标记),创建独立账号 |
取消 |
回登录页 |
账号绑定:登录后在用户控制台(dashboard)的「账号绑定」面板,GitHub 和 Gitea 各一张小卡片——已绑定的显示 @login 与已绑定徽标,未绑定的显示绑定按钮。绑定走的是 PocketBase 原生的 link 语义:携带当前 session 做 OAuth 授权交换,身份即挂到当前账号上,之后任一身份登录的都是同一个账号。/api/me 的 github_bound / gitea_bound 字段即面板数据源。
绑定到"别人的"账号
若绑定时该身份其实已挂在另一个账号上,PocketBase 会把会话切换成那个账号(而不是绑定);dashboard 会检测到并显示"会话已切换"警告,不做任何静默操作。
用户首次登录流程¶
sequenceDiagram
actor U as 用户
participant SPA as SPA
participant QA as qatlasd
participant GH as GitHub / Gitea
U->>SPA: 点 "使用 GitHub/Gitea 登录"
SPA->>QA: GET /api/collections/users/auth-methods(拿 provider authURL + PKCE)
QA-->>SPA: {authURL, state, codeVerifier}
SPA->>U: 302 → provider authorize URL(redirect_uri = https://qatlas/auth/callback)
U->>GH: 同意授权
GH-->>U: 302 → https://qatlas/auth/callback?code=xxx&state=xxx
SPA->>QA: POST authWithOAuth2Code(code + verifier + redirect_uri)
QA->>GH: POST /login/oauth/access_token (with client_secret)
GH-->>QA: { access_token: "..." }
QA->>GH: GET /user(GitHub)或 /api/v1/user(Gitea)
GH-->>QA: { id, login, email, name, avatar_url }
QA->>QA: 白名单校验 + 创建或匹配 users 记录(按 email)+ 盖 github_login/gitea_login
QA-->>U: session token(SPA 存 pb.authStore)
首次登录会自动创建 users 记录。之后浏览器 SPA 内自动持有 session(pb.authStore,无需手动 copy);非浏览器调用请在 /pat 页面创建 PAT 后使用。
多边缘节点¶
GitHub 与 Gitea 都限制每个 OAuth App 只允许一个 callback URL,所以多台边缘部署时必须为每台各建一个 OAuth App:
边缘 |
OAuth App name |
Callback URL |
client_id 写在哪 |
|---|---|---|---|
Edge 1 |
QuantumAtlas (Edge 1) |
|
Edge 1 的 config.yaml |
Edge 2 |
QuantumAtlas (Edge 2) |
|
Edge 2 的 config.yaml |
各边缘启动时只注入自己那一份 OAuth provider,互不冲突。同一账号在多边缘各登一次会建多条 users 记录(不同 user id),PAT 也是独立的。
排查¶
登录按钮没出现 / 少了 Gitea
对应的 client id/secret 没配(Gitea 还可能是漏了 gitea_url);或配了但 server 没 restart。检查:
journalctl -u qatlasd | grep -i oauth
# 应该有 "github oauth provider synced" / "gitea oauth provider synced" 日志
登录跳到 provider 后回来 404 / 500 / redirect_uri 不匹配
OAuth App 里注册的重定向 URI 与实际访问的 server URL 不完全一致。GitHub/Gitea App 设置里改成 https://<实际域名>/auth/callback(精确匹配,带协议、不带末尾斜杠)。
Gitea 授权回来后被 409 拦下(账号匹配提示)
这不是错误:你的邮箱或用户名与现有账号冲突。按提示"匹配已有账号"(登录后在控制台绑定),或选"继续,使用独立账号"。同邮箱的登录没有"独立账号"选项——users 表的邮箱列唯一。
GitHub 登录被拒(403)
GitHub 登录走白名单:把 GitHub 用户名加进 allowed_logins(或 admin_logins)。Gitea 名单对 GitHub 登录不生效。
登录后看不到 admin UI
OAuth 登录的是 users collection(普通用户),不是 PocketBase 内置 superuser。要 admin UI 用 qatlasd superuser upsert 建。
登录后 cookie 没设上
server 在反代后面但反代没 forward cookie / 没 preserve Host。看 反向代理 的三条铁律。
安全建议¶
Client Secret 视同密钥:不进 git,只存 server 的 config.yaml(mode 0600);rotate 时去 provider 的 App 设置页面重新生成。
callback URL 不要带通配符:精确匹配。
scope 最小化——GitHub/Gitea 都只要
read:user+user:email,QuantumAtlas 只需要 email + login,不会 access 你的 repo。
跟 caddy-security 风格 SSO 共存¶
如果你的反代上已经挂了 caddy-security 或其他 SSO portal(颁发 JWT、注入 X-Token-Subject 头),它跟 PocketBase OAuth 是正交的——caddy-security 管"谁能访问反代",PocketBase OAuth 管"谁是 QuantumAtlas 用户"。两层都过才能用写口(一层鉴权 + 一层凭据)。
如果 caddy-security 拦了 /api/oauth2-redirect 路径,要专门放行:
# 在 caddy-security 的 authentication 块之前插
handle /api/oauth2-redirect {
reverse_proxy 127.0.0.1:4200 { header_up Host {host} }
}
# 才是 caddy-security 的 authorize
handle /api/* {
authorize with quantum_atlas_policy
reverse_proxy 127.0.0.1:4200 { header_up Host {host} }
}
详见 反向代理。