被拒绝的 MFA 提示看起来像网络问题,其实不是。服务器要求第二个因素,该因素返回否定或根本没有到达,会话在隧道建立之前就被拒绝。接下来发生什么取决于客户端行为:把失败暴露出来,让用户开始一次干净的重试;或者以不断产生更多请求的方式重试,最终触发账户保护。后一条路径正是把一个输错的验证码变成一个被锁定的账户的方式。

先确认是哪一个请求失败

从识别哪一个请求失败开始。推送通知、TOTP 验证码、FIDO 断言以及SAML 流程的失败方式各不相同,客户端通常知道自己请求的是哪一种。拒绝、过期的挑战以及从未送达设备的投递失败,最终都表现为同一个笼统错误,因此差异往往只保留在客户端自身的状态里。

这份状态值得精确记录。记录挑战标识或事务参考号、因素类型、时间戳和结果,但绝不记录验证码或断言本身。挑战标识能让管理员在身份提供方一侧找到对应事件;没有它,支持对话只能从“大概在过去一小时里”开始,通常也只能到此为止。把它当作诊断元数据,而不是机密。

用新挑战,刻意重试一次

重试应当是一次带有新挑战的新尝试,而不是对旧挑战的重放。当客户端复用先前的挑战时,身份提供方要么以过期拒绝,要么更糟——接受一个本来属于前一次尝试的批准。请求新的挑战,把它显示出来,让用户针对此刻屏幕上的那一个进行操作。

两次尝试之间留一个短间隔是合理的默认值;无限制的循环不是。如果第二次尝试以同样方式失败,就停下来报告,而不是继续。

客户端不该做的事

客户端不该做几件看似有帮助、实则没有帮助的事。它不该一直重发推送通知直到有人点“批准”,因为这会训练用户去批准他们并未发起的请求,而这正是钓鱼工具包所依赖的行为。它不该因为强因素失败就退回到更弱的因素,也不该为了绕过同时出现的证书告警而关闭服务器身份校验。这三条捷径,每一条都会把一次认证失败变成一起安全事件。

它也不该默默吞掉失败。一个关闭连接并一直显示加载指示的客户端,会让用户完全不知道是该重试、等待还是联系支持。把失败连同因素类型和一个短代码显示出来,让提示信息不含机密,并把重试动作做成显式而非自动。

如果请求不是键盘前的人发起的,正确的做法是让它保持被拒绝,并通过经过核验的支持渠道上报。为了让提示音停下来而批准请求,是失败模式,不是修复。

需要为支持记录什么

对支持而言,有用的记录很小。带时区的近似时间、账户、客户端与版本、因素类型,以及每次尝试的非机密状态:已请求、被拒绝、已超时、已批准但被策略拒绝。

也要留意客户端与服务器的时间。TOTP 是时间窗口校验,所以时钟发生漂移的设备会生成看起来有效却每次都失败的验证码,而客户端在报告无效验证码时不提及时钟漂移,就会把人引去重置本来没有问题的凭据。反复登录失败的用户,应当先检查受支持的客户端版本、注册状态和已文档化的恢复流程,再生成更多请求。

恢复路径应当与登录路径得到同样的重视。丢失设备的处理流程之所以存在,正是为了不让一次拒绝变成绕过认证的许可,客户端应当链接到组织已文档化的流程,而不是自行发明一套。

应当据以构建的规则是:拒绝是一个结果,而不是障碍。一次性、清楚地报告它,并带上足以采取行动的上下文,让下一次尝试是新的尝试。CISA 关于抗钓鱼 MFA的指南解释了“一直问到批准为止”这种做法为何从一开始就危险,当有人提议重试循环时,这正是值得引用的参考文献。客户端在拒绝之后添加的一切,都应让人更容易做出下一个决定,而不是把决定权从人手里拿走。

相关文章