OAuth 2.0 协议原理与实现:Token 生成策略

OAuth2.0 协议定义了授权详细流程,并最终以 token 的形式作为用户授权的凭证下发给客户端,客户端后续可以带着 token 去请求资源服务器,获取 token 权限范围内的用户资源。

对于 token 的描述,OAuth 2.0 协议只是一笔带过的说它是一个字符串,用于表示特定的权限、生命周期等,但是却没有明确阐述 token 的生成策略,以及如何去验证一个 token。RFC6749 对于 access token 的描述:

The client obtains an access token – a string denoting a specific scope, lifetime, and other access attributes.

协议不去详细阐述 token 的生成和验证过程,个人觉得是因为这一块各个业务都有自己的特点,无法完全做到抽象,并且在这一块去做详细的规定,其意义并不大。Token 本质上就是对用户授权这一操作在时间和权限范围两个维度上的一个表征,协议可以对 token 的传递和基本验证做相应规定,但是具体的一个 token 包含哪些元素,采用什么样的生成算法还是需要由自己去把握。

本文主要讲解自己对于 token 生成的一些思考,以及介绍两种类型:BEARER 和 MAC。

一. TOKEN 的基本构成

Token 表征了用户授权这一操作,授权服务器通过下发 token 来给客户端颁发获取用户受保护资源的资格,且不会因此而泄露用户的登录凭证信息。Token 对于客户端应该是非透明的,客户端只知道这是一个字符串,能够用它来获取用户的受保护资源,对于字符串内部所含的信息应该无从知晓,也不能通过其它方法去解密其中的信息。所以 token 应该是一类对称加密得到的字符串,并且只有授权服务器持有对称密钥,用于对生成的 token 进行加密和验证。

对于构成token的元素,各个业务都有自己的需求,不过仍然存在一些基本通用的元素,比如:

  1. clientId:客户端 ID,当前 token 隶属的客户端
  2. userId:用户的 ID,表示当前 token 来自哪个用户授权
  3. scope: 权限范围,该 token 允许换取的用户受保护资源范围
  4. issueTime: 下发时间,用于控制 token 的生命周期
  5. tokenType: token 的类型,不同类型可能会采用不同的验证措施

以上是个人根据经验总结的一些基础的 token 组成元素,具体业务还可以根据具体的需求添加一些其他的元素。

二. Bearer Type Access Token

BEARER 类型的 token 是在 RFC6750 中定义的一种 token 类型,OAuth 2.0 协议 RFC6749 对其也有所提及,算是对 RFC6749 的一个补充。BEARER 类型 token 是建立在 HTTP/1.1 版本之上的 token 类型,需要 TLS(Transport Layer Security) 提供安全支持,该协议主要规定了BEARER类型token的客户端请求和服务端验证的具体细节。

2.1 客户端请求

客户端在携带token请求用户的受保护资源时,需要保证token的安全性,以防止token被窃取或篡改,从而损害用户数据安全。BEARER类型token定义了三种token传递策略,客户端在传递token时必须使用其中的一种,且最多一种。

2.1.1 放在Authorization请求首部

Authorization首部说明

Authorization首部是由客户端发送,以向服务器回应自己的身份验证信息,客户端在收到服务器的401 Authentication Required响应之后,需要在请求中包含该首部。

基本用法:Authorization:

在传输时,Authorization 首部的 authentication-scheme 需要设置为 Bearer,请求示例:

1
2
3
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
2.1.2 放在请求实体中

Token需放置在 access_token 参数后面,且 Content-Type 需要设置为 application/x-www-form-urlencoded,请求示例如下:

1
2
3
4
5
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM

协议推荐使用第一种方式,对于该请求方式,必须在满足如下条件时才允许使用:

  1. The HTTP request entity-header includes the “Content-Type” header field set to “application/x-www-form-urlencoded”.
  2. The entity-body follows the encoding requirements of the “application/x-www-form-urlencoded” content-type as defined by HTML 4.01.
  3. The HTTP request entity-body is single-part.
  4. The content to be encoded in the entity-body MUST consist entirely of ASCII characters.
  5. The HTTP request method is one for which the request-body has defined semantics. In particular, this means that the “GET” method MUST NOT be used.
2.1.3 放在URI请求参数中

该方式通过在请求URl后面添加 access_token 参数来传递token,请求示例如下:

1
2
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com

客户端在请求时需要设置 Cache-Control: no-store,服务端在成功响应时也需要设置 Cache-Control: private

由于很多服务都会以日志方式去记录用户的请求,此类方式存在较大的安全隐患,所以一般不推荐使用,除非前两种方案均不可用。

2.2 服务端验证

如果服务端拒绝客户端的访问请求,则需要在响应中添加 WWW-Authenticate 首部,响应示例如下:

1
2
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"

WWW-Authenticate首部说明

WWW-Authenticate首部用于401 Unauthorized响应,用于向客户端发送一个质询认证方案。

基本用法:WWW-Authenticate:

这里的响应,其中 auth-scheme 必须设置为 Bearer,如果客户端携带了无效的token,那么按照上一篇《OAuth 2.0 协议原理与实现:协议原理》 讲解的,OAuth 2.0 协议要求错误响应中必须携带 error 字段,并选择性携带 error_descriptionerror_uri,具体释义请参考上一篇,响应示例如下:

1
2
3
4
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"

三. MAC Type Access Token

前面介绍了BEARER类型的token,RFC6750明确说明该类型token需要TLS(Transport Layer Security)提供安全支持。虽然现今大部分站点都已经或正在由HTTP向HTTPS迁移,但是仍然会有站点继续在使用HTTP,在这类站点中BEARER类型的token存在安全隐患,这个时候MAC类型的token正是用武之地,MAC类型的token设计的主要目的就是为了应对不可靠的网络环境。

MAC类型相对于BEARER类型对于用户资源请求的区别在于,BEARER类型只需要携带授权服务器下发的token即可,而对于MAC类型来说,除了携带授权服务器下发的token,客户端还要携带时间戳,nonce,以及在客户端计算得到的mac值等信息,并通过这些额外的信息来保证传输的可靠性。

3.1 下发 MAC 类型令牌

OAuth2.0协议在规定下发accessToken时,包含 access_tokentoken_typeexpires_inrefresh_token,以及 scope 字段,其中部分字段可选,具体参见上一篇《OAuth 2.0 协议原理与实现:协议原理》,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

响应字段是可扩展的,对于MAC类型token则增加了 mac_keymac_algorithm 两个字段,mac_key 是一个客户端和服务端共享的对称密钥,mac_algorithm 则指明了加密算法(比如hmac-sha-1,hmac-sha-256),示例响应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"SlAV32hkKG",
"token_type":"mac",
"expires_in":3600,
"refresh_token":"8xLOxBtZp8",
"mac_key":"adijq39jdlaska9asud",
"mac_algorithm":"hmac-sha-256"
}

3.2 构造 MAC 类型请求

一些开放API接口可能会强制要求以MAC类型令牌来请求,这个时候就需要在客户端构造合法的请求,一个标准的请求示例如下:

1
2
3
4
5
6
7
GET /resource/1?b=1&a=2 HTTP/1.1
Host: example.com
Authorization: MAC id="h480djs93hd8",
ts="1336363200",
nonce="dj83hs9s",
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="

请求参数说明:

参数名 必须 描述信息
id 必须 访问令牌
ts 必须 时间戳
nonce 必须 客户端生成的字符串,对于相同token和timespan的请求nonce必须相同
ext 可选 扩展信息
mac 必须 根据MAC key和MAC algorithm计算出来的值

通过添加 id、ts、nonce、mac 字段到 Authorization 请求首部以发起对用户资源的请求,这里的 id 就是授权服务器下发的 accessToken;ts 则是时间戳,由客户端生成,以秒为单位;nonce 是客户端生成的一个字符串形式的签名,是对 ts 和 id 两个维度的唯一、可重复性标识;而 mac 则是整个客户端构造最核心和复杂的部分,可以看做是对本次请求参数的一个签名,1.2.3 小节专门讲解。此外客户端还以用 ext 字段来携带一些扩展数据。

3.3 mac 值算法

mac 值可以看作是对本次请求参数的一个签名,通过对请求数据进行本地加密计算得到,用于防止请求过程中参数被更改。服务器端收到请求之后,会以相同的算法和密钥重新计算一遍 mac值,并与客户端传递过来的作比较,如果不一致则拒绝该请求。因为密钥仅保存在客户端和服务端本地,所以无需担心mac值被更改或伪造,从而确保在没有TLS保证的环境下可靠传输,实际上这里可以看做是 MAC 类型请求自己实现了一遍 TLS。

mac值对于相同的请求参数必须是一致和可再计算的,对于参与计算元素的选择,协议选取了如下元素:

  1. The timestamp value calculated for the request.
  2. The nonce value generated for the request.
  3. The HTTP request method in upper case. For example: HEAD, GET, POST, etc.
  4. The HTTP request-URI as defined by RFC2616 section 5.1.2.
  5. The hostname included in the HTTP request using the Host request header field in lower case.
  6. The port as included in the HTTP request using the Host request header field. If the header field does not include a port, the default value for the scheme MUST be used (e.g. 80 for HTTP and 443 for HTTPS).
  7. The value of the ext Authorization request header field attribute if one was included in the request, otherwise, an empty string.

通过对这些元素按照顺序组织,并以换行符 \n 作分隔(最后一行也需要包含一个 \n),利用 mac_algorithm 指定的算法和 mac_key 指定的密钥对组织好的数据进行加密计算得到 mac 值。

计算示例:

假设有一个请求:

1
2
3
4
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q HTTP/1.1
Host: example.com

Hello World!

其中 ts=264095:7d8f3e4a,nonce=7d8f3e4a,ext=a,b,c

对该请求按照之前的说明进行组织,以\n分隔得到:

1
2
3
4
5
6
7
264095\n
7d8f3e4a\n
POST\n
/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n
example.com\n
80\n
a,b,c\n

其中 \n 仅仅是为了展示,实际中以 ASCII 码 %x0A 的意义表示,不要忘了最后一行的 \n。假设授权服务器指定的 mac_algorithm 为 hmac-sha-1,令 text 表示上面的字符串,那么最后的 mac 值得计算方式如下:

mac = hmac-sha-1(mac_key, text)

3.4 服务端验证

服务器端在收到客户端的请求之后,需要做如下验证:

  1. 重新计算mac值,并与客户端传递的值进行比较
  2. 确保(timestam, nonce, token)三个维度之前没有被请求过,以防止重放攻击
  3. 验证scope,以及token

如果服务端拒绝客户端的请求,则需要指定 WWW- Authenticate 响应首部,例如客户端携带了无效的授权信息,则服务器响应示例如下:

1
2
HTTP/1.1 401 Unauthorized
WWW-Authenticate: MAC error="The MAC credentials expired"

四. 本篇小结

本篇主要介绍了两种 token 类型,基本可以覆盖实际应用中的各种场景。Token 是对用户授权操作的一类凭证,一旦下发到客户端,其安全性就需要客户端去保证,为了尽量在保护用户数据和提升用户体验上寻找一个平衡点,token 的生命周期不应该设置的太短或太长。

本篇和上一篇《OAuth 2.0 协议原理与实现:协议原理》 介绍了 OAuth 2.0 协议涉及到的理论知识,相关实现可以参考考 oauth4j

参考文献

  1. RFC5849 - The OAuth 1.0 Protocol
  2. RFC6749 - The OAuth 2.0 Authorization Framework
  3. RFC6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage
  4. HTTP Authentication: MAC Authentication (draft-hammer-oauth-v2-mac-token-02)