跳转到内容

身份声明与共享

查看 Markdown

除了稳定的用户标识,应用有时还需要邮箱、名、姓、静态头像或动态头像。Sudomimus 把它们作为五条独立的身份声明(claim)管理。共享规则分为两部分:

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

只有应用策略允许、用户也同意时,UserInfo 才会返回真实资料。OIDC 还要求登录时申请对应的 scope。用户撤销授权后,下一次 UserInfo 请求就不再返回该资料。

在 With 门户的应用详情页中,每条声明都可以设为以下一种策略:

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

默认情况下,邮箱、名、姓和静态头像使用仅占位资料,动态头像为 Off。新应用因此可以获得稳定的占位身份,但不会自动收到真实资料。不需要的声明可以直接设为 Off。

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

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

他们的决定会被记为三种状态之一 —— 已授权、已拒绝或尚未决定。已拒绝的可选声明不会再次请求;尚未决定的声明会在下一次交互式登录时显示。

用户可以在账户门户的数据共享页面查看各应用能收到哪些资料,也可以按应用撤销授权。授权状态会实时用于 UserInfo;撤销后,下一次 UserInfo 请求就不再返回相应的真实资料。

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

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

flowchart TD
    Request["为会话权限与 UserInfo 评估一条声明"] --> OIDC{"是否为 OIDC 会话?"}
    OIDC -->|是| Scope{"是否请求了对应 scope?"}
    Scope -->|否| Omit["省略该声明"]
    Scope -->|是| Policy{"声明策略"}
    OIDC -->|否 — Session API| Policy

    Policy -->|OFF| Omit
    Policy -->|SYNTHETIC_ONLY| Placeholder["返回占位值"]
    Policy -->|OPTIONAL| Optional{"已授权且真实资料存在?"}
    Optional -->|是| Real["返回真实资料"]
    Optional -->|否| Omit
    Policy -->|REQUIRED| Required{"已授权且真实资料存在?"}
    Required -->|是| Real
    Required -->|否| Block["在 UserInfo 之前<br/>阻止签发或刷新"]
    Policy -->|SYNTHETIC_FALLBACK| Fallback{"已授权且真实资料存在?"}
    Fallback -->|是| Real
    Fallback -->|否| Placeholder

OIDC 的 scope 把关映射如下:

  • email scope → 邮箱声明
  • profile scope → 名、姓与头像

普通应用会话使用 Session API /userinfo,不受 OIDC scope 限制,只看声明策略和用户授权。OIDC 应用使用 discovery 中公布的 /userinfo 端点。

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

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 签发、刷新令牌时,响应会在令牌之外附带顶层 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 是派生字段, 没有独立状态。

  • Application access / refresh token —— 不携带个人资料 claim。OIDC ID Token 也不携带个人资料,但 response_type=id_token 是例外:它包含 scope、当前策略和用户授权允许披露的声明。已签发的 JWT 是快照;撤销授权不能收回已交付的数据。
  • 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 / 仅占位资料 / 占位兜底。
  • 用户 —— 账户门户里的数据共享视图:查看并撤销每个应用收到的内容。