OAuth 2.0 协议应用解析

前言

OAuth 2.0 全称是 Open Authorization 2.0, 是用于授权(authorization)的行业标准协议。 OAuth 2.0 专注于客户端开发人员的简单性,同时为 Web 应用程序、桌面应用程序、移动设备应用等提供了特定的授权流程。它在2012年取代了 OAuth 1.0, 并且 OAuth 2.0 协议不向后兼容 OAuth 1.0。

需要注意的是,OAuth 2.0 是一个授权(authorization)协议,而不是身份验证(authentication )协议。

Roles 角色

首先还需要了解一些概念, 因为整个OAuth授权流程都是围绕这些抽象的概念展开的, 角色是 OAuth2.0 授权框架核心规范的一部分, OAuth 定义了以下4种角色

  • Resource Owner
    资源所有者, 这里通常是拥有资源权限的用户或者系统。

  • Client
    客户端应用, 它可以通过访问令牌(Token)访问受保护资源, 可以是Web浏览器上的网站也可以是桌面应用或者手机App。

  • Authorization Server
    授权服务器, 在经过用户的授权后, 向客户端应用发放访问令牌(Access Token)。

  • Resource Server
    资源服务器, 存放受保护资源的服务器, 接受来自客户端(Client)请求的有效访问令牌(Access Token), 然后返回对应的资源。

Client Types 客户端类型

OAuth 2.0 核心规范定义了两种客户端类型, confidential 机密的, 和 public 公开的, 区分这两种类型的方法是, 判断这个客户端是否有能力维护自己的机密性凭据(password, client_secret)。

  • confidential
    对于一个普通的web站点来说,虽然用户可以访问到前端页面, 但是数据都来自服务器的后端api服务, 前端只是获取授权码code, 通过 code 换取access_token 这一步是在后端的api完成的, 由于是内部的服务器, 客户端有能力维护密码或者密钥信息, 这种是机密的的客户端。

  • public
    对于一个没有后端的纯前端应用来说(比如SPA), 数据的展示和操作都是在前端完成的, 包括获取令牌和操作令牌, 把一个客户端密码或者密钥放在纯前端应用是不安全的, 这种是公开的客户端。

Client Authentication 客户端身份认证

前面已经说过了, OAuth 2.0 是授权协议, 那为什么还要对 OAuth 2.0 客户端进行身份验证呢? 身份验证和授权有什么区别? 简单说身份验证确认用户是否是本人, 而授权则是授予用户访问资源的权限, 授权的前提条件一定是要先通过身份认证, 而且接下来的内容中, 也有用到了身份认证, 为了方便理解, 所以对认证做了简单的介绍。

授权服务器对客户端进行身份验证可以保证把令牌颁发给了合法的客户端, 但是认证其实已经超出了 OAuth2.0 的协议范围, 在 [RFC 6749] 中也只是简单介绍了以下2种认证方式:

第一种是使用 HTTP Basic [RFC2617] 中定义的身份验证方案进行身份认证, 这种方式叫 client_secret_basic, 首先需要对username,password 或者 client_id, client_secret 用冒号进行拼接。

{username}:{password} 或者 {client_id}:{client_secret} 就像这样 admin:123456
然后对字符串进行Base64编码, 然后设置为请求Header中的Authorization, 注意前面要拼接一个Basic和空格, 如下

POST /token HTTP/1.1
Host: www.authorization-server.com 
Authorization: Basic YWRtaW46MTIzNDU2
Content-Type: application/x-www-form-urlencoded 

第二种方式就更简单粗暴了, 直接在请求体中添加 client_id 和 client_secret 参数, 如下

POST /token HTTP/1.1
Host: www.authorization-server.com 
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw

Protocol Flow 协议流程

+--------+                               +---------------+
|        |--(A)- Authorization Request ->|   Resource    |
|        |                               |     Owner     |
|        |<-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant -->| Authorization |
| Client |                               |     Server    |
|        |<-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |<-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

上图是抽象的授权协议流程, 也展示了4种角色(Role)之间的交互, 具体的过程如下

(A) 客户端向资源所有者(用户)发起授权请求, 资源所有者选择授予权限或者取消, 这个过程中, 授权服务器充当中介的角色, user ---> authorization server ----> client.
(B) 客户端收到授权许可(code),这是一个代表资源所有者授权的凭证。
(C) 客户端通过授权许可(code)向授权服务器发起请求, 并期望获取一个访问令牌(access token)。
(D) 授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌(access token)并返回。
(E) 客户端通过访问令牌向资源服务器请求受保护的资源。
(F) 资源服务器验证访问令牌, 如果有效, 则返回相应的资源。

Access Token 访问令牌

access token 是一个用来访问受保护资源的凭证, 它是由授权服务器(Authorization Server)颁发给客户端(Client)的, 通常是字符串形式, access token 拥有特定的访问范围(scope), 并且有时间限制, 访问令牌可以有不同的格式、结构, 这点并没有限制。

Refresh Token 刷新令牌

refresh token是一个用来获取access token的凭证, 同样它是由授权服务器(Authorization Server)颁发给客户端(Client)的, 刷新令牌的时效性比访问令牌要长, 当访问令牌过期的时候, 可以直接用刷新令牌去授权服务器获取新的访问令牌, 而无需重新登录。和访问令牌不同的是, 授权服务器颁发访问令牌是必须的, 而颁发刷新令牌则是可选的, 并且访问令牌还会和资源服务器交互, 而刷新令牌只和授权服务器交互。

刷新令牌的设计非常巧妙, 它是用户体验和安全两方面取舍的一个平衡。

+--------+                                           +---------------+
|        |--(A)------- Authorization Grant --------->|               |
|        |                                           |               |
|        |<-(B)----------- Access Token -------------|               |
|        |               & Refresh Token             |               |
|        |                                           |               |
|        |                            +----------+   |               |
|        |--(C)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(D)- Protected Resource --| Resource |   | Authorization |
| Client |                            |  Server  |   |     Server    |
|        |--(E)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(F)- Invalid Token Error -|          |   |               |
|        |                            +----------+   |               |
|        |                                           |               |
|        |--(G)----------- Refresh Token ----------->|               |
|        |                                           |               |
|        |<-(H)----------- Access Token -------------|               |
+--------+           & Optional Refresh Token        +---------------+

(A) 客户端向授权服务器发起请求, 并提供授权许可。
(B) 授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌和刷新令牌。
(C) 客户端请求受保护资源并提供访问令牌。
(D) 资源服务器验证这个访问令牌,如果有效, 返回相应的内容。
(E) 重复步骤 (C) 和 (D),直到访问令牌过期。如果客户端知道了访问令牌已经过期,它跳到步骤(G), 如果不知道, 继续向资源服务器发起请求。
(F) 由于访问令牌无效,资源服务器返回无效的令牌错误。
(G) 客户端发起获取刷新令牌的请求, 同时要带上当前的刷新令牌。
(H) 授权服务器对客户端进行认证并验证刷新令牌,如果有效,则发出新的访问令牌和一个可选的新的刷新令牌。

Authorization Grant 授权许可

授权许可是一个资源所有者授权的凭证, 客户端通过它去获取访问令牌(access token), OAuth 2.0定义了以下四种许可模式。

  • Authorization Code 授权码
  • Implicit 隐式
  • Resource Owner Password Credentials 密码
  • Client Credentials 客户端凭证

Authorization Code Grant 授权码模式

授权码模式是最常用的一种授权许可模式, 也是最经典的一种, 这种模式可以获取到访问令牌和刷新令牌。还有一个特点是, 授权码模式是基于Web重定向的流程。

(A) 客户端提供一个授权链接, 引导用户点击跳转到授权服务的 /authorize 端点, 如下

https://www.authorization-server.com/oauth2/authorize?
response_type=code
&client_id=s6BhdRkqt3
&scope=user
&state=8b815ab1d177f5c8e 
&redirect_uri=https://www.client.com/callback

参数说明如下:

  • response_type:必选项, 表示响应类型,此处的值固定为"code"
  • client_id:必选项, 客户端的身份标识
  • redirect_uri 可选项, 经过用户允许授权后, 授权服务器跳转到客户端的回调地址
  • scope 可选项, 希望用户同意授权的权限范围
  • state 可选项, 推荐使用, 客户端可以维护一个在请求和回调之间的状态, 授权服务器重定向到回调地址时, 会带上这个参数, state 可以防止跨站点请求伪造-CSRF攻击。

(B) 授权服务器提供授权页面, 用户选择同意授权或者拒绝来自客户端的请求, 如下所示

(C) 假如用户同意了授权, 授权服务器会通过url重定向到客户端的回调地址, 并且会带上一个授权码 code 和 state 参数(如果之前客户端的请求中传递了state参数的话)

https://www.client.com/callback?
code=d8c2afe6ecca004eb4bd7024
&state=8b815ab1d177f5c8e 

(D) 现在已经拿到了授权码 code 并获得了用户的授权, 接下来需要用 code 来换取 访问令牌 access_token, 可以向授权服务的 /token 端点发送 POST 请求。

POST /token HTTP/1.1 
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

https://www.authorization-server.com/oauth2/token?
grant_type=authorization_code
&code=d8c2afe6ecca004eb4bd7024
&redirect_uri=https://www.client.com/callback

参数说明如下:

  • grant_type: 必选项,表示授权类型, 此处的值固定为"authorization_code"

  • code: 必选项,授权码, 这是上一步从授权服务器传给回调地址(redirect_uri)的参数

  • redirect_uri: 必选项, 客户端的回调地址, 注意要和(A)步骤中的 redirect_uri 一致。

  • client_id: 必选项,客户端的身份标识

注意, 上面使用了 Http Basic 身份认证(Authorization: Basic ...), 在本文的 "客户端身份认证" 部分有介绍, 主要是为了验证 Client 的合法性。

通过code换取access_token 步骤中,还有一种比较常见的身份验证做法是, 直接在请求体中传入 client_id, client_secret, 如下:

POST /token HTTP/1.1  
Content-Type: application/x-www-form-urlencoded

https://www.authorization-server.com/oauth2/token?
grant_type=authorization_code
&code=d8c2afe6ecca004eb4bd7024
&client_id=s6BhdRkqt3
&client_secret=ecca004eb4bd7024c2afe6ecc
&redirect_uri=https://www.client.com/callback

(E) 授权服务器对 client,code 验证通过后, 会返回 access_token 和一个可选的 refresh_token, 如下:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" 
}

参数介绍:

  • access_token: 必选项,访问令牌

  • token_type: 令牌类型, 通常是 Bearer [RFC6750], 访问受保护资源需要在请求头设置 (Authorization:Bearer ...)

  • expires_in: 访问令牌的有效期, 以秒为单位

  • refresh_token:可选的刷新令牌

(F) 客户端使用 access_token 向资源服务器发起请求
(G) 资源服务器验证 access_token, 验证通过后, 返回受保护的资源

这里有一个问题是, 文章上面说 access_token 只是一个字符串, 那么资源服务器如何来验证该令牌? 在 OAuth 2.0 核心协议中, 关于这点并没有提及。

访问令牌主要分为两种, 一种是没有意义的随机字符串, 比如 2YotnFZFEjr1zCsicMWpAA, 这种情况客户端本身是不能鉴别令牌是否有效, 只能去授权服务器发起请求来验证该令牌, 这种安全性高,但性能差, 可以参考 RFC 7662.

第二种就是很常见的 JWT 令牌, 可以参考 RFC 7519, 令牌本身就包含了一些用户信息, 资源服务器可以通过加密算法和签名验证令牌是否有效, 而且不需要和授权服务器进行交互, 但是缺点是, 如果令牌在到期前被撤销, 资源服务器是没办法知道的。

Implicit Grant 隐式授权模式

上面是隐式授权的流程图, 它和授权码模式很像, 区别在于, 授权码模式是先拿到code,然后再换取access_token, 而隐式授权只用一次请求就拿到了access_token, 通过url参数的形式返回, 令牌也直接暴露在了浏览器地址栏, 实际上这种模式是OAuth 2.0 对公开(public)的客户端的授权流程进行了优化, 上面说到了客户端分为两种, 机密的的和公开的, 因为公开的客户端没有能力维护自己的机密凭证, 所以适合这种模式, 并且授权码模式需要客户端认证 (通过code换取access_token的时候,需要使用 Http Basic认证,或者传入client_secret) , 而隐式授权在整个流程中并没有客户端认证,所以是不安全也不推荐使用的。

请求参数:

response_type 这里固定是 token

GET

https://www.authorization-server.com/oauth2/authorize?
response_type=token
&client_id=s6BhdRkqt3
&scope=user
&state=8b815ab1d177f5c8e 
&redirect_uri=https://www.client.com/callback 

响应参数:

https://www.client.com/callback
#access_token=2YotnFZFEjr1zCsicMWpAA
&state=8b815ab1d177f5c8e
&token_type=Bearer
&expires_in=3600

这里注意 access_token 实际上并不是一个url 参数, 它前面是 # 号, 表示一个fragment, # 有别于 ?? 后面的查询字符串会被网络请求发送到服务器,而 fragment 则不会发送到服务器, 但是js是可以解析到fragment的值, 也就是 access_token, 这个设计很巧妙!

Resource Owner Password Credentials Grant 密码凭证模式

+----------+
| Resource |
|  Owner   |
|          |
+----------+
    v
    |    Resource Owner
    (A) Password Credentials
    |
    v
+---------+                                  +---------------+
|         |>--(B)---- Resource Owner ------->|               |
|         |         Password Credentials     | Authorization |
| Client  |                                  |     Server    |
|         |<--(C)---- Access Token ---------<|               |
|         |    (w/ Optional Refresh Token)   |               |
+---------+                                  +---------------+

密码模式就更简单粗暴了, 用户直接把账号密码告诉客户端, 客户端向授权服务器发起POST请求, 并携带用户名和密码, 授权服务器验证通过后, 返回访问令牌和可选的刷新令牌, 这种模式的特点是, 用户和客户端是高度信任的。

请求参数:

POST /token HTTP/1.1
Host: www.authorization-server.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=johndoe&password=A3ddj3w

响应参数:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"Bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" 
}

Client Credentials Grant 客户端凭证模式

+---------+                                  +---------------+
|         |                                  |               |
|         |>--(A)- Client Authentication --->| Authorization |
| Client  |                                  |     Server    |
|         |<--(B)---- Access Token ---------<|               |
|         |                                  |               |
+---------+                                  +---------------+

客户端凭证模式的特点是, 客户端就是资源所有者, 客户端访问资源也不需要用户的授权, 因为这个过程中没有用户, 资源本身就属于客户端, 通过在请求体中传入 client_id,client_secret参数或者Http Basic 进行客户端认证, 这种模式很适合后端服务或者api之间调用的场景。

请求参数:

此处的 grant_type 固定是 client_credentials

POST /token HTTP/1.1
Host: www.authorization-server.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

响应参数:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"Bearer",
  "expires_in":3600,
  "example_parameter":"example_value"
}

总结

本文介绍了 OAuth 2.0 核心协议, 主要参考 RFC 6749 (The OAuth 2.0 Authorization Framework) 核心协议
, 相信读完本文, 你会发现有些流程其实是不安全的, 没错, 其中的隐式授权和密码授权模式已经不再建议使用, 因为隐式授权从一开始就没有真正安全过, 这里介绍一下背景, 当时 OAuth 2.0 出现的时间点在2010年左右, 移动端应用是全新的,单页面应用程序(SPA) 也才刚开始出现, 当时的Web生态和现在还是差别很大, 由于技术问题, 并不能使用常规的 OAuth 模式进行授权。 对于现在来说, 推荐使用专门为移动设备应用而设计的 PKCE (RFC 7636) 模式, 它是OAuth 2.0 核心的一个扩展协议, 也是最近几年移动设备应用授权的最佳实践。

posted @ 2021-11-16 09:15  晨煦风清  阅读(433)  评论(0编辑  收藏  举报