跳转到内容

身份声明与共享

查看 Markdown

除了一个稳定的标识符之外,应用往往还想要一点点资料 —— 用户的邮箱头像。Sudomimus 把这其中每一项都当作一条声明(claim),只有在双方都明确同意时才共享。四条声明各自独立把关,共享分为两半:

  • 声明策略(claim policy) —— 由开发者按应用设置:应用请求哪些声明,以及请求得有多强。
  • 声明授权(claim grant) —— 由用户按应用设置:他们同意共享哪些声明。

只有当策略请求了某条声明并且用户授权了它,这条声明才会进入令牌。对于 OIDC,还必须请求了相应的 scope。控制权在用户手里,被撤销的声明会在下一个令牌起就不再共享。

With 门户里你的应用详情页上,你把四条声明各设为以下之一:

策略含义
Off从不请求。无论用户是否愿意,这条声明永不共享。
Optional会请求,但用户可以拒绝。一旦拒绝,应用就直接收不到它。
Required应用需要它。用户必须授权才能完成登录。
仅占位资料 (SYNTHETIC_ONLY)始终提供生成的占位值,并且从不请求或共享用户真实数据。
占位兜底 (SYNTHETIC_FALLBACK)始终提供。用户授权时共享真实值;否则应用收到生成的替身(占位姓名、…@proxy.sudomimus.email 代理地址,或生成头像)。它从不阻塞登录,也从不触发 errand。

没有显式策略的应用会对每条声明使用仅占位资料,因此应用可以在没有真实资料授权的情况下收到稳定占位身份。若希望某条声明完全省略,请把它设为 Off

用户首次登录一个会请求声明的应用时,Sudomimus 会显示一个同意(consent)界面

  • Required 声明以锁定为开的形式展示 —— 授权它们是完成登录的一部分。
  • Optional 声明以复选框展示,默认不勾选 —— 由用户主动选择加入。
  • 占位兜底声明同样以可选复选框展示,差别在文案里点明:不勾选它,应用收到的是一个占位值,而不是什么都收不到。
  • 仅占位资料声明不会显示为同意选项,因为它从不共享真实数据。

他们的决定会被记为三种状态之一 —— 已授权已拒绝尚未决定 —— 所以被拒绝的可选声明不会被反复打扰,而从未见过的声明会在下一次交互式登录时再问。

用户可以在账户门户的数据共享视图里查看并修改每一个决定:它列出每个共享声明的应用、当前每条声明是否在共享、以及应用是否要求它,并为每个应用提供一个撤销操作。由于授权在每次签发令牌时都是实时读取的,撤销会立即生效 —— 下一个令牌就不再包含该声明。

对某条声明而言,规则是:

策略允许提供值;如果请求的是真实数据,则用户授权状态也允许;并且,对 OIDC 而言,请求了相应的 scope。

OIDC 的 scope 把关映射如下:

  • email scope → 邮箱声明
  • profile scope → 头像

非 OIDC 流程(Connect redeem、Session API refresh、原生 direct-issue)没有 scope 把关 —— 仅由策略 + 授权决定。

占位模式是「要么授权、要么省略」之外的例外:SYNTHETIC_ONLY 始终以占位值在场;SYNTHETIC_FALLBACK 在用户授权时提供真实数据,否则提供占位值。占位值按账户和扇区保持稳定:生成姓名、…@proxy.sudomimus.email 代理地址,以及生成头像 URL。占位声明从不阻塞登录、也从不被省略 —— 只是对 OIDC 邮箱而言,应用会被告知该地址未经验证。

Required 声明只能通过交互式授权满足。对于邮箱等数据型声明,账户还必须确实拥有相应数据。因此,非交互式签发点会拒绝任何可能生成缺少 required 声明令牌的登录,原因有两种:

  • ClaimConsentRequired —— 用户没有授权某条 required 声明。
  • RequiredClaimDataMissing —— 用户已经授权了它,但账户缺少底层数据(例如纯 Steam 账户没有邮箱)。

这项检查保证 required 声明在签发的令牌中一定存在,绝不会被静默省略。它适用于原生 direct-issue、令牌刷新和 OIDC token 端点。

用户如何解除阻塞,取决于客户端类型:

  • 原生客户端(Steam / AccessKey direct-issue)没有对应用本身的交互式登录,所以 403 会带上一个 Errand —— 一段浏览器侧行,用户在那里登录(如果要写入数据)、补全缺失的数据、并授予同意。随后客户端重试。对 AccessKey 而言,同样的同意也可以提前收集 —— 就在用户于门户里创建该 key 的那一刻。
  • 浏览器 / OIDC 客户端在下一次对该应用的常规交互式登录时解除阻塞,同意界面会内联展示。

未授权的可选声明则永远不会阻塞任何东西 —— 它只是被省略。占位模式同样从不阻塞,所以它们是「保证某个值一定在场」而又不强迫用户走一趟浏览器侧行的办法。

每当 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 字段名:emailgiven_namefamily_namepicturepicture_animated。OIDC 客户端从 discovery 读取提供方专属的 claim_state_endpoint;响应只包含该 session 的 emailprofile scope 所覆盖的状态。nameemail_verified 是派生字段, 没有独立状态。

  • 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_animatedname 由已授权的姓名部分组合而成。Synthetic 邮箱会以 email_verified: false 发出 —— 它是一个代理地址、不是已验证的邮箱,请不要当成已验证来处理。

最小令牌布局与 UserInfo 端点见令牌格式。头像交付还有额外的 URL 隔离、撤销与缓存规则,见头像声明与交付

  • 开发者 —— 你的应用在 With 门户的详情页:把每条声明设为 Off / Optional / Required / 仅占位资料 / 占位兜底。
  • 用户 —— 账户门户里的数据共享视图:查看并撤销每个应用收到的内容。