决定一切的那个问题
把客户端分为两种,而分界线是一个唯一的问题:这个客户端能否保守一个秘密?RFC 6749 把这两个答案称为机密客户端与公共客户端,而你属于哪一种,决定了你如何认证、哪些流程是安全的,以及你是否需要 。几乎每一个应用层面的 OAuth 设计错误,都可追溯到把一个公共客户端当作它能保守一个其实无法保护的秘密那样对待。
机密客户端能保守一个秘密
一个机密客户端运行在用户无法触及其代码或存储之处,经典地是一个服务器端的 Web 应用。它在注册时被签发一个客户端秘密,并用那个秘密向授权服务器的令牌端点认证自身。由于秘密只活在服务器上,授权服务器可以确信一个令牌请求确实来自那个已注册的客户端。这是强的情形:客户端用唯有它自己知晓之物来证明其身份。
公共客户端不能
一个公共客户端运行在其代码与数据对用户或攻击者可见之处:浏览器中的一个单页应用、一个原生的移动或桌面应用、任何发送到设备上的东西。其定义性问题在于:你嵌入这样一个客户端的任何秘密都不是秘密,因为任何人都能从所发送的捆绑包或二进制中将其提取。所以一个公共客户端根本无法用客户端秘密来认证;它能保守之物,攻击者也都能获得,无一例外。这改变了授权服务器所能假设的,也正是公共客户端在授权码流上需要额外保护的原因。
为何公共客户端曾经脆弱
授权码流在重定向中把一个一次性的码交给客户端,客户端随后用它换取令牌。对一个机密客户端而言,那次交换由客户端秘密保护。对一个无秘密的公共客户端而言,一个能拦截重定向的攻击者,例如一个在移动设备上为同一自定义 URL 方案注册的恶意应用,能窃取那个码并自行兑换它。这便是 PKCE 一文详述的拦截攻击,也正是一个缺失的客户端秘密所留下的敞口。
PKCE 填补这道缺口,为所有人
PKCE 无须一个预共享的秘密便堵上那个洞。客户端每次请求都发明一个新鲜的随机码验证者,在请求那个码时只发送它的哈希(码挑战),并仅在兑换它时才揭示验证者。一个拦截了那个码的攻击者从未见过验证者,于是被窃的码毫无用处。这是为公共客户端引入的,但此后指导意见已经拓宽:OAuth 2.1 推荐授权码流无论客户端类型为何都使用 PKCE,因为即便在客户端秘密存在之处,它也添加了纵深防御。务实的规则是处处使用授权码加 PKCE,并停止去推敲你是否严格需要它。
隐式流已退役
公共浏览器应用曾一度使用隐式流,它把令牌直接在重定向中返回,以避开码的交换。那种设计把令牌暴露在 URL 与浏览器历史中,如今已被弃用;OAuth 2.1 将其移除。对一个单页应用而言,现代的答案与对其他所有人相同:带 PKCE 的授权码流。若你遇到使用隐式流的建议,请把它当作过时之物对待。
原生应用有它们自己的陷阱
原生与移动应用是带有一个特定危险的公共客户端:返回应用的那次重定向。自定义 URL 方案可被设备上的其他应用所声称占用,这正是使拦截成为可能之物,所以针对原生应用的最佳实践指导(RFC 8252)是:在平台支持之处使用经声称的 HTTPS 重定向、在一个系统浏览器而非一个嵌入式 Web 视图中运行该流程,并始终应用 PKCE。对于输入受限的设备如一台电视,那里根本没有浏览器可供重定向,设备授权流(RFC 8628)让用户改在一部单独的手机或笔记本上授权。
拿不准时,假定为公共
贯穿始终的一点是:客户端类型不是你为了方便而挑选的一个标签;它是关于你的代码在何处运行、它能保护什么的一个事实。若客户端被发送给用户,它便是公共的,它无法保守一个秘密,它需要 PKCE。若它只在你的服务器上运行,它可以是机密的并用一个秘密认证,而它仍应使用 PKCE 以取得那一分额外的余量。判定你的客户端身处哪个世界,它 OAuth 安全模型的其余部分便从那一个诚实的答案随之而出。