第三方登录
两件方向相反的事,都在后台「安全 → 登录方式」里:
- 用别处的账号登录这里——登录页上出现「使用 GitHub 继续」「使用 Google 继续」。本站是客户端,去对方那里问「这是谁」。
- 用这里的账号登录别处——别人的站点上出现「用 Obsidian Arc 登录」。本站是身份提供方,别的软件来问我们。
两者互不依赖,可以只开一个。默认都是关的。
用别处的账号登录这里
配置
先在对方平台建一个 OAuth 应用:
| 在哪里建 | 回调地址填 | |
|---|---|---|
| GitHub | github.com/settings/developers → New OAuth App | https://你的域名/api/auth/oauth/callback/github |
| Google Cloud 控制台 → 凭据 → OAuth 客户端 ID(Web 应用) | https://你的域名/api/auth/oauth/callback/google |
回调地址在后台那两个字段的提示里直接写着,照抄即可——它是服务器自己算出来的地址,也就是令牌交换时真正会发给对方的那一个。
把 Client ID 和 Client Secret 填进后台,保存,再打开对应的开关。开关和凭据是分开的:粘贴密钥只是在配置,还没有对外开门;两项都齐了登录页才会出现按钮。Secret 保存后不再回显,只显示打码——留空提交表示「不改」,不会把它清掉。
账号是怎么对上的
认人靠的是对方平台的用户 ID,不是邮箱。邮箱会易主,按邮箱认人等于把账号跟着邮箱转手。
第一次用某个 GitHub 账号登录时:
- 这个 GitHub 账号在本站绑定过 → 直接登录那个账号。
- 没绑定过,但对方提供了已验证的邮箱,而本站已有账号在用这个邮箱 → 认为是同一个人,绑定并登录。这就是「按邮箱识别已有账户」开关,默认开;关掉后这种情况会提示邮箱已被占用,让对方用原来的方式登录后再去设置里绑定。
- 都不是 → 新开一个账号。
**未验证的邮箱一律当作没有邮箱。**没验证的邮箱只是一句声称,谁都能在自己的 GitHub 上填别人的地址;真按它认人,填谁的地址就能进谁的账号。所以这种情况下新账号是没有邮箱的,用户可以之后自己在设置里填,走正常的邮箱验证流程。
OIDC 也是同一条规则,而且不取决于「信任 OIDC 邮箱」(oauth.oidc_trust_email,默认关)。那个开关只决定未验证的邮箱会不会被存进账号。用户名则只从已验证的邮箱(@ 之前的部分)得出,否则取对方给出的显示名。对方既不给 preferred_username、又只给未验证的邮箱时,新账号的用户名就是显示名;显示名不是合法用户名的,会被带到补充表单让用户填写,不会直接开号。以前用的是邮箱的前半段,升级后同样的登录可能多出这一步。
新账号照样要过注册那一套规则:注册开关、邮箱域名白名单、每 IP 注册数、注册频率,「仅限邀请」模式下还要有邀请码。用户名从对方的用户名生成,重名就加后缀。
开了「仅限邀请」时,一次会新开账号的第三方登录会先停在补充表单上问邀请码,而不是直接开号——和邮箱、QQ 缺失时的表现一样,见邀请码。已经绑定过的账户不受影响,照常登录。
关掉「允许第三方登录注册新账户」时会拒绝——已绑定的账号仍可登录,只是不再开新号。
开了注册人机验证(Turnstile,或工作量证明)时,会新开账号的第三方登录要从注册页发起:注册页上的第三方按钮会先完成验证再跳去对方,验证不过就不离开本站。从登录页发起、又会新开账号的,回到本站时会提示「请从注册页面开始」,什么都不写入。已绑定过的账号照常登录。
对方给不了的东西
站点要求填 QQ 号,或者要求邮箱而对方没给出已验证的邮箱时,不会拒绝,而是停下来问:跳回本站后出现一个只剩这一两项的表单,填完才真正开户。
这中间什么都还没写。身份暂存在一个签名过的 Cookie 里(20 分钟过期),关掉页面走人不会留下任何账号。填完之后照样要过注册的全部规则:号码的格式、有没有被人占用、邮箱域名白名单、每 IP 注册数。
这里填的邮箱和注册表单里填的一样,是没有验证过的:它不会被用来认领已有账号(那会变成「填谁的邮箱就能进谁的账号」),开启邮箱验证时也照样会收到验证链接。只有对方平台声明已验证的邮箱才算数。
不跑注册审查模型。那个模型判断的是「这个用户名和邮箱像不像人编的」,而这里没有人编任何东西,GitHub 已经替我们确认了这是个有历史的账号。
没有密码的账号
通过第三方登录新开的账号没有密码——不是随机生成一个没人知道的,是根本没有。这类账号在登录表单里的反应和密码错了完全一样,所以没法用登录框试出哪些账号是第三方登录的。
用户可以在「设置 → 账户」里自己设一个密码,那里不会要求填「当前密码」——从来没有过的东西没法确认。设完就多了一条独立的登录路径,即使你之后关掉了 GitHub 登录,人也进得来。
在「设置 → 账户」里绑定新的第三方登录,也要先确认是账户本人:有密码的账户要填当前密码;没有密码的账户要求刚刚登录过,超过 15 分钟的登录不算数,重新登录一次再绑定。所以只拿到一份旧会话 Cookie 的人绑不上去。
同样的道理,账户的最后一种登录方式不能解绑:没有密码、只绑了一个 GitHub 的账号,解绑按钮会拒绝并告诉用户先设密码。否则那个账号不是被停用也不是被删除,只是谁都进不去了,东西还都在里面。
用这里的账号登录别处
本站实现的是 OpenID Connect。用标准协议而不是自己发明一套,图的就是对方不用为我们写代码:多数软件只要填一个 issuer,剩下的自己发现。
发现地址:https://你的域名/.well-known/openid-configuration
打开它看一眼 issuer 是不是 https:// 开头。如果是 http://,说明服务器 没能从请求里看出自己是 HTTPS——代理链把协议弄丢了——这时候对方会拒绝, 在部署环境里填上 OBSIDIAN_PUBLIC_URL=https://你的域名 即可。回调地址那一栏 显示的也是服务器自己算出来的地址,和这里是同一个来源。
| 端点 | 路径 |
|---|---|
| 授权 | /oauth/authorize |
| 令牌 | /oauth/token |
| 身份 | /oauth/userinfo |
| 公钥 | /oauth/jwks |
| 吊销 | /oauth/revoke |
身份令牌用 RS256 签名,密钥由服务器自己生成,存在数据库里并用实例密钥加密——因为它是要公开验证的,两个实例连同一个数据库时必须用同一把。
登记一个应用
在「安全 → 登录方式 → 登录应用」里填:
- 名称:授权页上显示给用户看的,用户就是靠这个决定要不要点「允许」。
- 说明:一行,显示在名称下面。
- 回调地址:一行一个,完全匹配。多一个结尾斜杠就是另一个地址——对接失败十有八九是这个。除 localhost 外必须是 https。
- 不签发 client secret:纯前端应用、手机应用这类没地方藏密钥的,勾上它,改用 PKCE。
- 跳过授权确认页:只用于自己的服务。勾上之后用户不会被询问就会把身份告诉它。
保存后会显示 client secret,只显示这一次。服务器只存摘要,之后谁也拿不回来;丢了就点「重签密钥」再来一次,旧的立刻失效。
可以申请的信息有四项:
| scope | 对方会知道 |
|---|---|
openid | 你是哪个账号(一个不会变的标识,必需) |
profile | 用户名、昵称、头像 |
email | 邮箱地址,以及是否已验证 |
groups | 在本站所属的用户组,对方可以据此映射自己的角色 |
用户看到什么
点了对方站点上的按钮,跳到本站的授权页:谁在请求、会知道哪些信息、跳回哪个域名。没登录的话会先要求登录,登录后回到同一个请求——不是回到聊天页。
同意过一次就不再问,除非对方后来申请了更多信息。用户在「设置 → 账户 → 已登录的站点」里能看到全部授权并随时移除;移除不只是从列表里划掉,对方手上的令牌会立刻失效,等于把它登出。
拒绝的几种情况
- client_id 不认识,或回调地址没登记过:不跳转,直接在本站的页面上说明。这两种正是攻击者希望服务器帮忙跳一下的情形,所以在确认回调属于该应用之前,服务器不会往任何地方跳。
- 其他参数有问题(response_type 不支持、scope 不对):按协议跳回该应用自己的回调,附带错误码——此时回调已经验证过是它自己的。
- 授权码被用第二次:整个应用在这个账号下的令牌全部吊销。授权码值一次;出现第二次说明有人手里有一份副本,而无法判断哪一次是真正的应用。刷新令牌同理,每次刷新都会轮换,旧的再出现同样全部吊销。
终端
app list、app show、app create、app edit、app secret、app delete,需要 security 权限;但设为受信任(跳过授权页)、修改回调地址,以及改动已受信任应用的任何内容,只有超级管理员能做——授权按应用记录,新增的回调会收到此前同意过的人的授权码。app create 和 app secret 会打印 client secret,同样只打印一次。
用户自己那一侧在 oauth list 和 oauth unlink——绑定需要浏览器,所以终端里只能看和解绑。