身份声明与共享
除了一个稳定的标识符之外,应用往往还想要一点点资料 —— 用户的邮箱、名、姓或头像。Sudomimus 把这其中每一项都当作一条声明(claim),只有在双方都明确同意时才共享。四条声明各自独立把关,共享分为两半:
- 声明策略(claim policy) —— 由开发者按应用设置:应用请求哪些声明,以及请求得有多强。
- 声明授权(claim grant) —— 由用户按应用设置:他们同意共享哪些声明。
只有当策略请求了某条声明并且用户授权了它,这条声明才会进入令牌。对于 OIDC,还必须请求了相应的 scope。控制权在用户手里,被撤销的声明会在下一个令牌起就不再共享。
声明策略(开发者)
Section titled “声明策略(开发者)”在 With 门户里你的应用详情页上,你把四条声明各设为以下之一:
| 策略 | 含义 |
|---|---|
| Off | 从不请求。无论用户是否愿意,这条声明永不共享。 |
| Optional | 会请求,但用户可以拒绝。一旦拒绝,应用就直接收不到它。 |
| Required | 应用需要它。用户必须授权才能完成登录。 |
仅占位资料 (SYNTHETIC_ONLY) | 始终提供生成的占位值,并且从不请求或共享用户真实数据。 |
占位兜底 (SYNTHETIC_FALLBACK) | 始终提供。用户授权时共享真实值;否则应用收到生成的替身(占位姓名、…@proxy.sudomimus.email 代理地址,或生成头像)。它从不阻塞登录,也从不触发 errand。 |
没有显式策略的应用会对每条声明使用仅占位资料,因此应用可以在没有真实资料授权的情况下收到稳定占位身份。若希望某条声明完全省略,请把它设为 Off。
用户首次登录一个会请求声明的应用时,Sudomimus 会显示一个同意(consent)界面:
- Required 声明以锁定为开的形式展示 —— 授权它们是完成登录的一部分。
- Optional 声明以复选框展示,默认不勾选 —— 由用户主动选择加入。
- 占位兜底声明同样以可选复选框展示,差别在文案里点明:不勾选它,应用收到的是一个占位值,而不是什么都收不到。
- 仅占位资料声明不会显示为同意选项,因为它从不共享真实数据。
他们的决定会被记为三种状态之一 —— 已授权、已拒绝或尚未决定 —— 所以被拒绝的可选声明不会被反复打扰,而从未见过的声明会在下一次交互式登录时再问。
用户可以在账户门户的数据共享视图里查看并修改每一个决定:它列出每个共享声明的应用、当前每条声明是否在共享、以及应用是否要求它,并为每个应用提供一个撤销操作。由于授权在每次签发令牌时都是实时读取的,撤销会立即生效 —— 下一个令牌就不再包含该声明。
声明何时真正出现在令牌里
Section titled “声明何时真正出现在令牌里”对某条声明而言,规则是:
策略允许提供值;如果请求的是真实数据,则用户授权状态也允许;并且,对 OIDC 而言,请求了相应的 scope。
OIDC 的 scope 把关映射如下:
emailscope → 邮箱声明profilescope → 名、姓与头像
非 OIDC 流程(Connect redeem、Session API refresh、原生 direct-issue)没有 scope 把关 —— 仅由策略 + 授权决定。
占位模式是「要么授权、要么省略」之外的例外:SYNTHETIC_ONLY 始终以占位值在场;SYNTHETIC_FALLBACK 在用户授权时提供真实数据,否则提供占位值。占位值按账户和扇区保持稳定:生成姓名、…@proxy.sudomimus.email 代理地址,以及生成头像 URL。占位声明从不阻塞登录、也从不被省略 —— 只是对 OIDC 邮箱而言,应用会被告知该地址未经验证。
Required 声明与非交互式登录
Section titled “Required 声明与非交互式登录”Required 声明只能通过交互式授权满足。对于邮箱等数据型声明,账户还必须确实拥有相应数据。因此,非交互式签发点会拒绝任何可能生成缺少 required 声明令牌的登录,原因有两种:
ClaimConsentRequired—— 用户没有授权某条 required 声明。RequiredClaimDataMissing—— 用户已经授权了它,但账户缺少底层数据(例如纯 Steam 账户没有邮箱)。
这项检查保证 required 声明在签发的令牌中一定存在,绝不会被静默省略。它适用于原生 direct-issue、令牌刷新和 OIDC token 端点。
用户如何解除阻塞,取决于客户端类型:
- 原生客户端(Steam / AccessKey direct-issue)没有对应用本身的交互式登录,所以
403会带上一个 Errand —— 一段浏览器侧行,用户在那里登录(如果要写入数据)、补全缺失的数据、并授予同意。随后客户端重试。对 AccessKey 而言,同样的同意也可以提前收集 —— 就在用户于门户里创建该 key 的那一刻。 - 浏览器 / OIDC 客户端在下一次对该应用的常规交互式登录时解除阻塞,同意界面会内联展示。
未授权的可选声明则永远不会阻塞任何东西 —— 它只是被省略。占位模式同样从不阻塞,所以它们是「保证某个值一定在场」而又不强迫用户走一趟浏览器侧行的办法。
claims 块
Section titled “claims 块”每当 Sudomimus 通过 Connect、Session API 或原生 direct-issue 签发或刷新令牌(/redeem、/refresh、/direct-issue/*)时,响应都会在令牌之外附带一个顶层 claims 块。它回答了缺失声明本来留下的疑问:这条声明为什么不在我的令牌里?
{ "email": { "requirement": "REQUIRED", "state": "GRANTED" }, "firstName": { "requirement": "OPTIONAL", "state": "DENIED" }, "lastName": { "requirement": "OFF", "state": "UNKNOWN" }, "staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" }, "animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }}每个声明都会给出它的 requirement(开发者的策略:SYNTHETIC_ONLY / OFF / OPTIONAL / REQUIRED / SYNTHETIC_FALLBACK)与它的 state(用户的当前决定:UNKNOWN / GRANTED / DENIED)的组合。UNKNOWN(「从未问过」)与 DENIED(「明确拒绝」)之所以要区分,正是这里用三种状态而不是一个可空布尔值的原因。
这个块描述 UserInfo 会采用的策略与同意状态:策略为 OFF、用户从未作出选择、用户拒绝授权,或已经授权但账户缺少数据。direct-issue 因声明检查返回 403 时,同一个块会列出建立 session 前仍需满足的条件。
如需在签发后读取实时状态,请携带 access token 调用 Session
GET /claim-state。它的 claims map 使用 UserInfo 字段名:email、
given_name、family_name、picture 与 picture_animated。OIDC 客户端从
discovery 读取提供方专属的 claim_state_endpoint;响应只包含该 session 的
email 与 profile scope 所覆盖的状态。name 与 email_verified 是派生字段,
没有独立状态。
应用收到什么
Section titled “应用收到什么”- Application access / refresh token 与 OIDC ID token —— 不携带个人资料 claim。
- Session
/userinfo—— 由 claim policy 与用户授权实时把关,不受 OIDC scope 控制。 - OIDC
/userinfo—— 由 scope 与授权共同把关:邮箱声明变成email(外加email_verified);名变成given_name;姓变成family_name;静态头像变成picture;动态头像变成picture_animated;name由已授权的姓名部分组合而成。Synthetic 邮箱会以email_verified: false发出 —— 它是一个代理地址、不是已验证的邮箱,请不要当成已验证来处理。
最小令牌布局与 UserInfo 端点见令牌格式。头像交付还有额外的 URL 隔离、撤销与缓存规则,见头像声明与交付。
- 开发者 —— 你的应用在 With 门户的详情页:把每条声明设为 Off / Optional / Required / 仅占位资料 / 占位兜底。
- 用户 —— 账户门户里的数据共享视图:查看并撤销每个应用收到的内容。