跳转到内容

身份认证的设计原则

查看 Markdown

Sudomimus 把每一道协议边界都视为独立暴露面:浏览器、应用服务器、原生客户端、OIDC 接入方以及它们之间的网络,都可能分别被攻陷。因此,一条流程不应让任何单一参与方掌握足以冒充其他所有参与方的材料。

这些原则横跨 Connect、OIDC 与原生 direct-issue。部分章节以 Connect 作为具体例子;协议细节则放在各自的集成章节中。

许多身份认证系统都会使用长期凭据——OAuth 的 client_secret、API key,或应用环境变量中的固定签名密钥。凭据泄露后,在轮换完成前可能持续授权操作;具体影响还取决于协议采用的其他证明与检查。

Connect 不会把某个长期共享密钥本身当成兑换所有登录的充分证明。每次往返都会在 /establish 时生成全新的 hiddenKey,只在 /redeem 中使用一次,随后永久失效。某次会话的 hiddenKey 泄露只会影响这一次兑换,不会危及应用过去和未来的所有登录。

这是一种持有证明设计:使用短期、范围明确的密钥,而不是长期、广泛共享的密钥。

不采用的方案: 使用同一个长期 client_secret 验证所有令牌交换。

Sudomimus 中的 账户(account) 记录的是「你是谁」——一个带名字的稳定身份。认证方式(authentication method) 记录的是「你怎么证明」——通行密钥、邮箱地址、Steam 身份、AccessKey 凭据。两者是不同的记录,一个账户可以挂多种认证方式。

这样做的好处:

  • 邮箱所有权不嵌入账户记录。 这消除了一条直接按邮箱查询 Account 的数据路径。枚举防护仍取决于端点的可观察行为、速率限制和流程控制。
  • 切换认证方式不影响身份。 用户在邮箱验证码账户上新增一把通行密钥,只是新增一条认证记录。账户本身不变,与之绑定的所有信息也都不变。
  • 整个数据库里没有「密码」这一列。 Sudomimus 不存密码。没有密码可泄露——因为根本就没有密码。目前支持通行密钥、邮箱验证码、社交登录(Google、GitHub、Discord、Battle.net、X)、Steam、AccessKey 凭据,未来还会接入更多。

不采用的方案: 把邮箱作为账户身份,或存储密码记录。

3. 身份是不透明的,而且对每个应用都不一样

Section titled “3. 身份是不透明的,而且对每个应用都不一样”

你的应用拿到的是一个成对标识符(pairwise identifier)——一个稳定、不透明的 sub,它只在你这个应用范围内唯一。同一个人登录两个不同的应用,会拿到两个互不相关的标识符;应用只使用自己扇区内的那个标识。

这样做的好处:

  • 无法跨应用关联。 两个应用即使串通,拿各自的标识符互相比对,也无法把「它们各自的用户」拼成同一个真人。
  • 标识符在契约上就是不透明的。 它是一个用于精确匹配的令牌,而不是一个供你解析的结构化值。Sudomimus 可以改动它的内部格式而不破坏你的接入——正因为你本来就不该从里面读出任何东西。
  • 令牌泄露只会暴露某个应用所看到的用户身份,不会暴露平台级身份。

不采用的方案: 向每个接入方暴露同一个全局用户标识符。

4. 用户决定每个应用能了解到什么

Section titled “4. 用户决定每个应用能了解到什么”

个人资料只通过 UserInfo 返回,并且仅限于用户已同意分享给那个特定应用的声明,以及应用策略明确请求的占位值。邮箱、名、姓、头像各自独立受控,且授权随时可以撤销。UserInfo 每次调用都会实时解析策略和授权,因此撤销会立即生效。

这样做的好处:

  • 应用请求的声明可能不存在。 应用必须处理可选声明缺失的情况。如果应用依赖某条声明,请将其设为必需;在用户同意前,Sudomimus 会阻止非交互式签发,而不是返回不完整的 UserInfo。
  • Bearer 凭据暴露得更少。 Access、refresh 和 ID token 都不携带个人资料;客户端只在需要时从 UserInfo 获取当前资料。

不采用的方案: 用户登录后向每个应用发布完整资料。

5. 验证用密码学,而不是用数据库查询

Section titled “5. 验证用密码学,而不是用数据库查询”

当应用收到 access token 时,它是一个签名后的 JWT。应用从 JOSE header 读取 kid、从 payload 读取 aud,再从 GET /applications/{applicationAnchor}/jwks.json 返回的应用 Session JWK Set 中精确选择对应密钥,并按响应头缓存整组密钥。验证在本地完成,不需要为每个已认证的应用请求调用 Sudomimus。

由此带来:

  • 每个已认证请求都不需要额外网络调用——验证完全在本地完成。
  • 登录之后你的服务对 Sudomimus 不存在可用性依赖。
  • 令牌被设计为短期有效(access token 默认有效期是几个小时,不是几天)。过期时,一次对 /refresh 的 HTTPS 调用即可获取替代令牌,无需用户重新认证。

OIDC 的 ID token 是另一种情况:接入方通过 oidc.sudomimus.com/.well-known/jwks.json 上的 JWKS 验证签名。完整说明见 令牌与验证

不采用的方案: 每个已认证请求都需要查询远程 IdP 会话。

6. 访问采用允许列表,默认拒绝

Section titled “6. 访问采用允许列表,默认拒绝”

谁可以完成认证、可以使用哪些方式、结果如何返回,都由显式允许列表控制。未配置规则的应用不会允许任何人登录;访问保持关闭,直到管理员明确启用。

不采用的方案: 默认允许访问,之后再由管理员添加限制。

一种常见的反模式是账户锁定:失败次数过多会在一段时间内冻结整个账户。这可以减少暴力破解尝试,但也会形成账户级拒绝服务风险。

每次 Sudomimus 认证会话都有自己的尝试次数。失败的尝试只会消耗当前会话的次数,不会锁定账户。次数用尽后,当前会话失效,用户可以开始新的会话。

不采用的方案: 由单次认证会话中的失败触发账户级锁定。

要伪造一次成功的 /redeem,攻击者必须同时持有:

  • 一把只存在于应用服务器上的密钥(hiddenKey
  • 一个只发送给特定那一个浏览器的引用(exposureKey
  • 一份只有 Sudomimus 在用户通过真实挑战后才会签发的证明(confirmationKey

任何单一的失误都凑不齐这三样东西。URL 泄露不会影响服务器的密钥;服务器被攻破不会牵连其他用户的会话;用户被钓鱼也不会拖累服务器。

机制细节在 三密钥模型 里讲清楚了;但更通用的原则是:把一份证明拆成三份,分散到三个独立的信任域里

不采用的方案: 使用单个会话令牌承载全部兑换权限。

9. 信任边界是被执行出来的,不是写在文档里的

Section titled “9. 信任边界是被执行出来的,不是写在文档里的”

Sudomimus 仅有六个公开的认证与集成服务——connect-api.sudomimus.comsession-api.sudomimus.comvia.sudomimus.comdevice-api.sudomimus.comnative-api.sudomimus.comoidc.sudomimus.com。集成方可访问的能力都通过这六个服务之一公开;产品支持和账户管理入口不授予集成权限。其他服务无法从平台外部访问,这一边界在平台边缘强制执行。

实际效果是:完成一次认证只有少数几条明确定义的通路,集成方无法绕过它们。

不采用的方案: 仅依靠文档或调用方约定保护内部 API。

接入 Sudomimus 的应用具有以下安全特性:

  • 你永远见不到密码,所以也不需要安全地保存任何密码。
  • 应用获得不透明、按应用区分的标识符,而不是可用于跨站关联的全局标识符。
  • 你手里只会有用户同意分享给你的那些身份声明。
  • 你的应用本地验证令牌;Sudomimus 即便宕机也不会把已登录用户挡在你的后端之外。
  • 单个会话的 hiddenKey 独自泄露时,影响范围限于对应的 Connect 会话;长期应用凭据仍需妥善保管并及时轮换。
  • 你不需要自己写账户锁定逻辑。
  • 应用自有端点仍需采用常规的枚举与滥用防护;Sudomimus 会在自身协议边界保护邮箱发现流程。

接下来可以选择接入方式,或直接查看具体的 Connect 流程