strong-factor-lifecycle

强因子的生命周期与重认证窗口

所谓“强因子”(strong factor),指任何比密码更强的认证方式:TOTPpasskey,或已绑定的 OAuth 账号。系统为每个用户记录:是否至少有一个这样的因子处于激活状态、它最近一次被证明是在什么时候,并在放行敏感的账户变更之前,要求一次新鲜的证明。本节说明这套状态是如何被推导、保持更新、并加以强制的。

值对象

StrongFactor 是一个领域值对象,持有三个字段:complete(是否有任一强因子激活)、methodtotp | passkey | oauth | null)、completedAt(该因子最近一次被证明的时刻)。它要么被构造为 incomplete()——空状态——要么通过 fromProps() 从已存储字段构造。complete(method, completedAt) 返回一个新的“已完成”实例;isWithinReauthWindow(windowMs) 回答“上一次证明是否足够新鲜”:没有时间戳时返回 false,否则把已过去的时间与调用方给定的窗口做比较。窗口并不写死在领域里——由调用方决定证明保鲜多久。

推导:状态是算出来的,而非盲目断言

完成状态不是某人手动翻动的开关,而是从底层记录重新算出来的。快照层为一个用户读取三个来源:是否存在一个已验证的 TOTP 密钥、passkey 数量是否大于零、是否有任何已绑定的 OAuth 账号。只要其中任一存在,该用户即为“完成”。当多个来源同时存在时,上报的 method 遵循固定优先级——先 TOTP,再 passkey,最后 OAuth。

两条写入路径:断言 vs. 刷新

存储状态的改变恰好只有两种方式:

  • 断言recordStrongFactorAssertion)——用于某个因子刚被证明或刚被启用。它强制把 complete 置为 true,写入给定的 method,并把当前时刻盖为新的 completedAt,覆盖此前的时间戳。这正是重置重认证计时的动作。
  • 刷新refreshStrongFactorState)——用于某个因子可能被移除时。它根据当前来源重新计算快照并持久化,因此解绑最后一个强因子会把用户翻回“未完成”。

时间戳规则是刻意设计的:普通刷新时,若用户本就已完成,则保留原有的 completedAt,因此仅仅重算状态并不会悄悄延长保鲜窗口。只有断言(或显式强制)才会把计时往前推。

两条路径都把相同的三个字段(completemethodcompletedAt)写入两条记录——User 记录与 Credential 记录——让这两份冗余副本保持一致。

观察者:生命周期变化被自动捕捉

启用与解绑不必自己记得去更新强因子状态。一个 Prisma 中间件(registerStrongFactorObserver)监听对 PasskeyAccount 模型的写入。创建/upsert 一个 passkey 或 account 会触发一次断言(method 分别为 passkeyoauth);删除则触发一次刷新。它从操作中抽取受影响的用户 id 并逐一更新。TOTP 不走这条监听——它的已验证状态在推导时被读取,并由验证它的流程来断言。

变更边界处的重认证窗口

在一次敏感账户变更之前,应用层会用已存储的用户字段构造出一个 StrongFactor,并检查两件事。第一,因子必须“完成”,否则请求直接被拒。第二,如果上一次证明落在一个较短的重认证窗口(五分钟)之内,请求无需重新证明即可通过。超出该窗口,用户必须提供一次新鲜证明——例如一个当前有效的 TOTP 码——一旦验证通过,就被记录为一次新的断言,从而重置窗口。效果是:证明一次强因子,就换来一小段宽限期,用于连续做几件敏感操作;期满之后,必须再次展示“持有”。

为什么是这个形状

把完成状态存成推导出来的状态、而非手工设置的标志,意味着真相总能回溯到真实的 TOTP/passkey/账号记录——标志与现实无从背离,因为刷新会重新算它。重认证窗口把“拥有”一个强因子与“最近证明过”它分开:一个长期存活的会话在触碰任何危险操作前,仍可被强制展示一次实时的持有证明。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →