三种令牌,三种职责
授权码流完成之后(见码流一文),一个客户端可能最终持有三种不同的令牌,而混淆它们是 与 OpenID Connect 中最常见的错误之一。访问令牌、刷新令牌与 ID 令牌表面上相似,尤其当三者恰好都是 时,但它们有不同的受众与不同的用途。把它们弄对,大体上是为每个令牌问一句:它是给谁的、它授权什么。
访问令牌:通往一个 API 的钥匙
访问令牌是客户端发往资源服务器(一个 API)以证明它获准发出请求的东西。它是 OAuth 所拥有的、最接近用于机器对机器访问的会话密钥之物,由 OAuth 2.0(RFC 6749)定义。两个性质要紧。它短命,常为数分钟到一小时,使得一个泄露的令牌只在短时间内有用。它又是一个 bearer(持有者)令牌:谁持有它便能使用它,无须进一步的身份证明,这便是为何访问令牌只能在 TLS 上传输,且永不被记录或放入 URL。API 校验该令牌及它所携带的作用域,然后服务或拒绝该请求。
刷新令牌:取得新的访问令牌
由于访问令牌很快过期,强迫用户每隔几分钟重新登录会令人难以忍受。刷新令牌解决了这点:它是一个长寿命的凭据,客户端在授权服务器处用它换取一个新鲜的访问令牌,无须用户交互。正因其长寿命,它远比访问令牌敏感,所以它必须留在后端信道(服务器一侧,或在受保护的存储中),且永不暴露给一个浏览器前端。良好实践是刷新令牌轮换:每次使用都签发一个新的刷新令牌并使旧的失效,于是一个被盗的令牌会在合法客户端尝试使用那个现已被吊销的副本之际即被察觉。令牌也可被显式吊销(RFC 7009)。
ID 令牌:一份关于用户的陈述
ID 令牌是 OpenID Connect 加在 OAuth 之上的那一块,也是最常被误用的一块。它始终是一个 JWT(见 JWT 解剖一文),它描述认证事件:用户是谁、他何时认证,以及哪个授权服务器为之担保。至关重要的是,ID 令牌是给客户端的,而非给一个 API。它回答“谁刚刚登录”,以便应用能建立一个用户会话。把一个 ID 令牌当作访问令牌那样发往资源服务器是一个经典的 bug:API 应当拒绝它,因为 ID 令牌的受众是客户端,而把它用于授权,是把身份与权限混为一谈。
bearer 令牌与信任它们的诱惑
多数访问令牌是 bearer 令牌,这使它们易于使用、也易于误用。处理规则直接随之而来:始终用 TLS、短寿命、永不进入日志或查询串,且若存在一个更受保护的选项便永不进入浏览器可访问的存储。在 bearer 模型过于冒险之处,绑定发送方(sender-constrained)的令牌把一个令牌绑定到某个特定的客户端密钥(诸如 DPoP 或双向 TLS 的机制),使得一个被盗的令牌无法被任何他人重放。对多数应用而言,TLS 上的短命 bearer 访问令牌是可行的基线。
不透明令牌与 JWT 访问令牌
一个访问令牌可以是不透明的(一个随机字符串,API 通过调用授权服务器的内省端点来核对它,RFC 7662),或是一个由 API 自行校验的 JWT。JWT 形式自包含、避免了一次网络往返,但在它过期之前无法被吊销,这是把访问令牌寿命保持得短的又一个理由。相比之下,ID 令牌始终是一个 JWT,因为它的全部职责就是把可验证的 claims 传达给客户端。一个令牌是否为 JWT,告诉你的是它如何被校验,而非它为何而设。
令牌与受众相匹配
防止大多数令牌 bug 的那一条规则,是把每个令牌与它意图中的受众相匹配。ID 令牌是给应用的,用以得知谁登录了。访问令牌是给 API 的,用以授权一个请求。刷新令牌是给授权服务器的,用以铸造新的访问令牌而不打扰用户。把这三个受众理清、把长寿命的刷新令牌留在后端信道、并把每个 bearer 令牌当作一个短命的秘密对待,OAuth 令牌处理的其余部分便各归其位。