OAuth 2.1 框架


OAuth 2.1 Draft

当前版本:v2-1-05
失效时间:2022/09/08

本文对部分原文翻译,同时加了一些笔记,以便理解。

单词 译意
identifiler 识别码
Resource Owner 资源拥有者
User-Agent 用户代理
Authorization Code 授权码
Access Token 访问令牌
refresh token 刷新令牌
scope 可选
endpoint 端点
AS 授权服务器

许可类型

要获取访问令牌,客户端需要从资源拥有者那里获得授权。本规约定义了以下几种授权许可类型。

  • 授权码(authorization code)
  • 客户端证书(client credentials)
  • 刷新令牌(refresh token)

本规约还提供了扩展机制,以便定义其他许可类型。

授权码许可

授权码许可类型用于获取访问令牌和刷新令牌。

许可类型使用额外的授权端点,实现授权服务器与资源拥有者交互,以便获取资源访问准许。

由于这是一个基于重定向的工作流,客户端必须能够通过资源拥有者(比如某个用户)的用户代理(一般指 web 浏览器)初始化工作流,并且能够从授权服务器重定向回来。

授权码流程图

OAuth 2.1 框架

注意:图中所示的步骤(1)、(2)、(3)在通过用户代理的时候会分为两个部分。

图中包括的步骤如下:

(1)客户端通过将资源拥有者的用户代理指向授权端点来发起授权流程。请求携带客户端自己的识别码、code challenge(来自生成的 code verifier)、请求范围(可选)、local state(可选,这里的意思是可以传递一些客户端的数据,回调的时候会把这些数据原样传回来)、回调 URI,当授权服务器许可(或拒绝)的时候会向该 URI 发送用户代理。

关于 code verifier,可查看下一节 [[#授权请求]] 

(2)授权服务器认证资源拥有者的身份(通过用户代理),并确定资源拥有者是许可还是拒绝客户端的访问请求。

(3)假设资源拥有者许可访问,则授权服务器通过之前(在发起请求时或者客户端注册期间)提供的重定向 URI 将用户代理重定向回客户端。重定向 URI 里包括授权码和客户端之前提供的任何 local state。

(4)客户端从授权服务器的令牌端点请求访问令牌,请求中需要携带上个步骤中获取的授权码、以及客户端自己的 code verifier。当发起请求时,如果授权服务器可以认证身份,客户端将通过授权服务器认证身份。客户端为了验证,将携带重定向 URI 以便获取授权码。

(5)授权服务器尽可能的认证客户端的身份,验证授权码、code verifier,并且保证接收到的 URI 与步骤(3)中重定向到客户端的 URI 是匹配的。如果通过认证,授权服务器返回反问令牌,以及刷新令牌(可选)。

授权请求

要发起授权请求,客户端需要将参数添加到授权服务器的授权端点 URI 上,以此构建授权请求 URI。客户端最终会将用户代理重定向到此 URI 上来发起请求。

这里看上去不是很好理解,我提供一个示例 ``` java 	private final String authorizationRequestUri = UriComponentsBuilder   	  //授权服务端点 URI       .fromPath("/oauth2/authorize")  	  //参数       .queryParam("response_type", "code")        .queryParam("client_id", "messaging-client")         .queryParam("scope", "openid message.read message.write")         .queryParam("state", "state")         .queryParam("redirect_uri", this.redirectUri)         .toUriString(); 

客户端每次发起授权请求都使用唯一的密钥,以此避免授权码注入,CSRF 攻击。客户端先生成此密钥,它可以在使用授权码时使用它来证明使用授权码的客户端就是请求它的客户端。

客户端通过 application/x-www-form-urlencoded 格式,添加以下参数到授权端点 URI 的查询组件中,构造客户端的请求 URI。

参数 是否必填 说明
response_type 授权端点支持不同的请求集合和响应数据。客户端根据 response_type 的值来决定授权流程。本规约定义了值的代码,该代码必须用于指示客户端要使用授权码流程。

"response_type":必填。授权端点支持不同的请求集合和响应数据。客户端根据 response_type 的值来决定授权流程。本规约定义了值的代码,该代码必须用于指示客户端要使用授权码流程。

扩展的响应类型可能是包含空格分隔符(%x20)的列表,这些响应类型的值在列表中顺序不会产生影响(例如,响应类型 a b 等同于 b a)。这类组合响应类型的含义有它们各自的规范定义。

某些扩展响应类型由 OpenID 定义。

如果授权请求缺少 response_type 参数,或者如果响应类型无法理解,授权服务器必须返回错误响应。

参数 是否必填 说明
client_id 客户端识别码
code_challenge 是或推荐 Code challenge
code_challenge_method 可选 默认值 plain,Code verifier 转换方法为 S256 或 plain
redirect_uri 可选
scope 可选
state 可选 客户端用于维护请求与回调之间的状态。授权服务器在重定向用户代理回客户端时将此值加入请求中

code_verifier 是唯一的熵很高的加密随机字符串,每次授权请求生成一次,使用 unreserved 字符包括 [A-Z]、[a-z]、[0-9]、“-”、“.”、“ _ ”、“~”,最小字符串长度为 43,最大字符长度 128。

1948年,香农Claude E. Shannon引入信息(熵),将其定义为离散随机事件的出现概率。一个系统越是有序,信息熵就越低;反之,一个系统越是混乱,信息熵就越高。所以说,信息熵可以被认为是系统有序化程度的一个度量。 

客户端临时存储 code_verifier,计算用于授权请求的 code_challenge。

用于 code_verifier 的 ABNF(巴科斯范式)如下。

code-verifier = 43 * 128unreserved unreserved = ALPHA / DIGIT / "-" / "." / " _ " / "~" ALPHA = %x41-5A / %x61-7A DIGIT = %x30-39 

注意:code verifier 的熵应该足够高,以至于值不会被猜到。建议使用合适的随机数生成器来创建一个 32 octet 的序列。每个 octect 序列使用 base64url 编码后生成一个 43 octet 的 URL 安全的字符串作为 code verifier。

1 octet = 8 bits  为什么不使用 byte,因为 byte 的语义存在歧义,历史上的 byte 不是固定的 8 位。 

客户端然后在 code verifier 的基础上创建 code_challenge:

S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

plain
code_challenge = code_verifier

如果客户端能够使用 S256,则必须使用 S256,因为服务器上强制执行(Mandatory To Implement,MTI) S256。客户端只能在由于某些技术原因不支持 S256 的情况下,才能使用 pain,例如,受环境限制不能使用哈希函数,并且通过带外配置或者授权服务器元数据得知服务器支持 plain。

用于 code_challenge 的 ABNF(巴科斯范式)如下。

code-challenge = 43 * 128unreserved unreserved = ALPHA / DIGIT / "-" / "." / " _ " / "~" ALPHA = %x41-5A / %x61-7A DIGIT = %x30-39 

code_challenge 和 code_verifier 的属性吸取了 OAuth 2.0 的扩展”Proof-Key for Code Exchange“,也叫做 PKCE,也是这项技术的起源地。

授权服务器必须支持 code_challenge 和 code_verifier 参数。

客户度必须使用 code_challenge 和 code_verifier,除了一些在 7.6 节 中描述的条件外,服务器也必须强制客户端使用 code_challenge 和 code_verifier。在当前情况下,我们仍然推荐按照下面列出的方式强制使用 code_challenge 和 code_verifier。

state 和 scope 参数不应该在 plain 文本中包含和客户端、资源拥有者相关的敏感信息,因为它们能够通过不安全的通道传输,或者以不安全的方式存储。

客户端通过 HTTP 重定向或者用户代理提供的其他方式,指示资源拥有者构造 URI。

例如,客户端指示用户代理发起以下 HTTP 请求(额外的断行符只是为了显示的目的):

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb        &code_challenge=6fdkQaPm51l13DSukcAH3Mdx7_ntecHYd1vi3n0hMZY        &code_challenge_method=S256 HTTP/1.1    Host: server.example.com 

授权服务器验证请求,以确保所有的必须的参数都有效。

特别地,如果请求中存在 redirect_uri,授权服务器必须验证,确保其值与客户端注册的 URI 匹配。当比较两个 URI 时,授权服务器必须一个字符一个字符的比较。

如果请求有效,授权服务器认证资源拥有者的身份,并且获取授权决策(通过询问资源拥有者或者通过其他方式建立批准)。

这种获取授权决策,具体体现,比如:使用微信登录其他 App 时,会跳转到微信 App,询问用户是否允许。 

当完成决策后,授权服务器指示用户代理使用 HTTP 重定向响应或用户代理提供的其他方式,提供客户端重定向 URI。

授权响应

如果资源拥有者许可访问请求,授权服务器将颁发一个授权码并传送给客户端,使用 application/x-www-form-urlencoded 格式,在重定向 URI 查询组件中添加以下参数:

参数 是否必填 说明
code 由授权服务器生成的授权码,并且对客户端不透明。授权码在颁发后,必须在短期内失效,以防泄漏。推荐授权码的生命周期为 10 分钟。客户端只能使用一次授权码。如果使用授权码超过一次,授权服务器必须拒绝请求,并且应该撤销(如果可能的话)基于上次颁发的授权码生成的所有访问令牌和刷新令牌。授权码与客户端识别码、code challenge、重定向 URI绑定。
state 如果客户端授权请求中包含 state 参数。确切值来自客户端。

例如,授权服务器通过发送以下 HTTP 响应重定向用户代理:

HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA              &state=xyz 

客户端必须忽略不能识别的响应参数。本规约未定义授权码字符串的大小。客户端应该避免假定授权码的大小。授权服务器应该记录颁发的授权码的大小。

服务器关联颁发的授权码与 code_challenge 的确切方法,超出了本规约的范围。code challenge 应该存储在服务器上,并且在服务器上关联授权码。code_challenge 和 code_challenge_method 的值可以以加密的方式存储在代码自身中,但是服务器不能在响应参数重包括 code_challenge 的值,只能提取 AS 以外的实体。

客户端必须防止攻击者注入授权码到授权响应中。使用 code_challenge 和 code_verifier 可以阻止注入授权码,原因是如果 code_verifier 不匹配,授权服务器将拒绝令牌请求。

错误响应

如果请求由于重定向 URI 缺失、无效、或者不匹配失败,或者如果客户端识别码缺失、无效,授权服务器应该通知资源拥有者错误,并且不能自动将用户代理重定向到错误的 URI。

AS 必须拒绝不携带 code_challenge 来自公共客户端的请求,并且必须拒绝来自其他客户端的这类请求,除非能保证客户端客户端不会以其他方式注入授权码。

如果服务器不支持请求的 code_challenge_method 转换方法,授权端点必须返回错误响应,并将 error 的值设为 invalid_request。error_description 或者 error_uri 应该解释错误的本质,比如,不支持的转换算法。

如果资源拥有者拒绝访问请求,或者如果请求失败是因为除了重定向 URI 缺失或失效之外的原因,授权服务器应该使用 application/x-www-form-urlencoded 格式,向重定向 URI 查询组件中添加以下参数:

参数 是否必填 描述 错误代码 错误代码描述
error