OAuth 2.0 认证授权

OAuth 是用于授权的行业开放标准协议,在这个标准中,用户可以在第三方应用访问该用户在某一平台上存储的资源,也就是我们经常看到的某些平台可以使用第三方账号登陆。
例如我们在学校中的一些平台可以使用易班账号进行登陆,登陆之后该平台能拿到我们存储在易班上的学生信息或对账号在易班平台上的资源进行其它操作。

该标准目前使用的版本是 2.0,目前网上关于该标准的介绍有很多,尤其是 okta 平台上的文章,对于 OAuth 2.0 的每个知识点几乎都有一篇文章来介绍,当然也因为 okta 本身就是一个面向开发者的授权认证平台。

文章声明

  • 由于 OAuth 是用于认证授权的,和客户端架构关系不大,所以本篇文章基于 B/S 架构的应用来讲解
  • 为方便理解,我们在此声明一个例子,为下面的介绍提供具体对象的例子。例子:我们去访问知乎网站时,使用微博账号进行登陆
  • 由于客户端和平台的界线在一部分人中存在模糊性,所以进行特殊声明:在本文章中的客户端可以理解为一个提供服务的应用个体,例如知乎的网站(包括后台),平台为承载服务的整体,两者关系为平台包含客户端,例如知乎的网站是知乎的一个应用,但在大部分情况下两者可理解为同一样东西
  • 对于用户浏览的平台来说,用户登陆的账号所在的平台是第三方,所以有些页面显示其它登陆方式的时候会显示第三方账号登陆,但是对于用户来说授不授权取决于自己,完成授权操作的是用户,而提供授权和用户数据操作权限的是账号所在的平台,所以在 OAuth 的授权流程中,用户浏览的平台才是第三方
  • 文章部分定义参考自 RFC 6749,关于请求和响应包含的参数,应当作为例子参考,具体参数必须根据应用接入时服务方提供的API文档里的规定,相关定义觉得不妥请在评论区提出(其它联系方式也可)

名词概念

学习 OAuth 2.0 首先要理清楚一些相关的名词概念。

User-Agent

用户代理,在 B/S 架构下的整个授权流程中,由于用户是使用浏览器进行操作,所以在这里用户代理指的是浏览器

Client

指的是第三方客户端,也就是我们浏览的平台,相对于上面例子中的知乎网站

Resource Owner

资源所有者,也就是用户(User)

Authorization Server

授权服务器(认证服务器),用来对 Resource Owner 的身份进行验证,颁发 code(授权码)和 access token(授权令牌 / 访问令牌)(文章中的 token 也是指授权令牌)

Resource Server

资源服务器,服务商提供存放用户资源的服务器,资源服务器和授权服务器可以为同一台服务器,也可以是不同服务器。

授权类型

在 OAuth 2.0 框架中多个授权方式,其中通用的有 4 种授权方式:

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

授权码模式

在所有授权模式中,授权码模式是流程最严密的一个授权模式了,由于用户在浏览器方面只需要获取一个授权码,获取访问令牌的过程是在服务器进行,用户是不可见的。

    Authorization Code Flow (来自 RFC 6749)

    +----------+
    | Resource |
    |   Owner  |
    |          |
    +----------+
         ^
         |
        (B)
    +----|-----+          Client Identifier      +---------------+
    |         -+----(A)-- & Redirection URI ---->|               |
    |  User-   |                                 | Authorization |
    |  Agent  -+----(B)-- User authenticates --->|     Server    |
    |          |                                 |               |
    |         -+----(C)-- Authorization Code ---<|               |
    +-|----|---+                                 +---------------+
      |    |                                         ^      v
     (A)  (C)                                        |      |
      |    |                                         |      |
      ^    v                                         |      |
    +---------+                                      |      |
    |         |>---(D)-- Authorization Code ---------'      |
    |  Client |          & Redirection URI                  |
    |         |                                             |
    |         |<---(E)----- Access Token -------------------'
    +---------+       (w/ Optional Refresh Token)

授权码模式的授权流程如下,该授权流程为RFC 6749提出的授权流程:

(A) 用户访问客户端(Client),客户端将浏览器重定向到认证服务器的页面,重定向的 URL 会带上以下参数

  • response_type:授权类型,必选项,在这个授权模式下固定值为”code”
  • client_id:客户端 ID,必选项,用来标识是哪个客户端请求授权
  • redirect_uri:重定向地址,可选项,用来让浏览器携带 code 重定向到客户端的接收地址,通常情况下都必须加上这个参数
  • scope:授权的权限范围,可选项
  • state:请求与回调之间的状态值,可选项,推荐加上,授权服务器 code 的时候也会返回这个值,该参数应用于防止跨站点请求伪造

(B) 用户选择是否给客户端进行授权,如果用户同意授权,那么进入 (C)
(C) 授权服务器将浏览器重定向到原本客户端指定的 redirect_uri(重定向地址),在地址上附带上授权码和 code(授权码)设定的 state
(D) 客户端服务器拿到 code,带上这个 code 和先前的 redirect_uri, 向认证服务器申请访问令牌
(E) 认证服务器核对 code 和 redirect_uri 通过后,将 access_token(访问令牌)响应给客户端,响应包含以下数据

  • access_token:访问令牌,必选项
  • token_type:令牌类型,必选项
  • expires_in:令牌的过期时间,必选项
  • refresh_token:更新令牌,可选项,用来获取下一次访问令牌
  • state:客户端指定的 state,如果客户端没有指定则不加上
  • scope:权限范围

由于此模式安全性非常高,所以大部分授权都会要求有服务端的应用采用这一种方式,在这个授权过程中用户不需要提供服务端密码给客户端,只需要提供一个代替账号密码的 code,让客户端拿这个 code 去授权服务器进行验证,获取用户的令牌就可以了,这样在不需要当心账号密码会被客户端保存下来。

隐式授权模式

在 OAuth 2.0 规范中包括隐式授权这一个模式,但通常不会直接使用这一模式,许多文章都提到不应当使用传统的隐式授权,原因在于传统的隐式授权返回的 token 是用户可见的,传统的隐式授权的流程如下:

Implicit-Grant-LegacyFlow
(上图来源于参考资料 4)

  1. 用户访问客户端进行登陆操作
  2. 客户端将用户引导到授权页面
  3. 用户确认授权
  4. 授权服务器携带 token 重定向到客户端

在这个授权过程中,可以看到客户端并没有参与授权,授权过程全都是在浏览器进行,所以这一授权方式适用于不带后端服务的 SPA 应用。

但是在这一个过程中,我们可以看到客户端获取 token 是通过授权服务器在 URL 中携带 token 重定向到客户端,这一操作使得 token 暴露在外,用户可以看见这一 token,当然中间人也可以,所以这一方式在网络中是不安全的。

虽然不建议采用这种方式,但是我们可以在隐式授权的基础上进行修改,让它变得更加安全,在OAuth 2.0 — OAuth,提供许多相关资料,其中包括RFC 6749中提出的关于隐式流的授权方式和PKCE授权方式。

我们先来看看 RFC 6749 在提出的隐式授权方式。

    Implicit Grant Flow (来自 RFC 6749)

    +----------+
    | Resource |
    |  Owner   |
    |          |
    +----------+
         ^
         |
        (B)
    +----|-----+          Client Identifier     +---------------+
    |         -+----(A)-- & Redirection URI --->|               |
    |  User-   |                                | Authorization |
    |  Agent  -|----(B)-- User authenticates -->|     Server    |
    |          |                                |               |
    |          |<---(C)--- Redirection URI ----<|               |
    |          |          with Access Token     +---------------+
    |          |            in Fragment
    |          |                                +---------------+
    |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
    |          |          without Fragment      |     Client    |
    |          |                                |    Resource   |
    |     (F)  |<---(E)------- Script ---------<|               |
    |          |                                +---------------+
    +-|--------+
      |    |
     (A)  (G) Access Token
      |    |
      ^    v
    +---------+
    |         |
    |  Client |
    |         |
    +---------+

(A) 用户访问客户端,客户端将浏览器重定向到授权页面,其中重定向地址中携带着参数,这些参数和授权码模式中第一步的参数类似

  • response_type:授权类型,必选项,在这个授权模式下固定值为”code”
  • client_id:客户端 ID,必选项,用来标识是哪个客户端请求授权
  • redirect_uri:重定向地址,可选项,用来让浏览器携带 code 重定向到客户端的接收地址,通常情况下都必须加上这个参数
  • scope:授权的权限范围,可选项
  • state:请求与回调之间的状态值,可选项,推荐加上

(B) 用户进行登陆授权
(C) 授权服务器重定向到客户端指定的 redirect_uri,并带上一个包含 access_token 的片段,当然这一片段是加密的,在返回的 URI 中还包含以下参数

  • access_token:访问令牌,被进行了加密
  • token_type:令牌类型,必选项
  • expires_in:令牌的过期时间,必选项
  • scope:权限范围
  • state:客户端指定的 state,如果客户端没有指定则不加上

(D) 客户端将这一片段保存在浏览器中,不携带这一片段,继续向资源服务器发送请求
(E) 资源服务器会返回一个 HTML 页面,里面包含了一段脚本(可能是 JavaScript 脚本)
(F) 浏览器会执行这一段脚本提取前面得到的片段中的 access_token
(G) 然后浏览器把 access_token 发送给客户端

这一方式将原本包含在 URI 中可见的 access_token 进行了加密,并让用户再次进行请求获取一个用于解密的脚本,这一流程为 RFC 6749 的流程,但是我们重新审查这一流程会发现有模糊点:

  • 在原文没有明确指出步骤 (D) 重定向到资源服务器时需要携带哪些参数,这样无法确认需要提取的是哪一个 access_token
  • 没有指出存在 HTML 页面中的代码是一段可调用或初始化就进行执行的 JavaScript,还是一段密钥类型的字符串,需要提取后用预先规定的算法进行解密
  • 在原文中,请求脚本使用的是重定向方式,然而这一方式出现的问题依旧是 Get 请求获取的数据容易被恶意截获,这就造成其它人也可以对即便是加密但依旧是明文传输的 access_token 进行解密操作了

对于程序员来讲,解决这些问题并不困难,我们可以对不足的地方从新设计,但现在先抛开 RFC 6749 这一隐式授权模式,来看看 PKCE 授权

PKCE
(上图来源于参考资料 5)
PKCE 模式的授权流程如下:

  1. 用户访问客户端
  2. 客户端生成一个随机值 v 存储在浏览器中,对这个值用 SHA-256 算法加密得到 $
  3. 携带着加密的随机值 $ 重定向到授权页面
  4. 授权服务器保存这一加密的随机值 $,返回授权页面
  5. 用户进行登陆授权
  6. 授权服务器返回一个授权码 code
  7. 浏览器携带这一授权码 code 重定向到客户端
  8. 客户端向授权服务器发送一个 POST 请求,请求携带的数据包括客户端 ID、一开始生成的随机数 v,授权码 code
  9. 授权服务器会校验客户端 ID、code、使用 SHA-256 算法加密后的 $,校验成功返回 token

这一过程中的关键在于一开始的 $ 和后面发送请求传入的 v,由于 v 是存储在客户端或者浏览器,所以其它人无法获得,对于授权服务器来讲,只需要后面客户端发送请求传入的 v 进行加密后的值 $ 和一开始进行授权请求时提供的 $ 一致,那么就可以确认这一次请求是该客户端发送的。
接下来只需要验证 code 对应的用户即可,由于获取 token 的操作为客户端进行 POST 请求,那么返回的 token 也是不可见的,所以在这一个流程下是安全的

相比 PKCE 和 RFC 6749 的隐式授权,可以发现 PKCE 更容易理解,出现的问题也比较少,安全性更有保证,并且 OAuth 社区也有人提出用 PKCE 代替隐式授权。个人建议也是在需要采用隐式授权,那么应当优先考虑 PKCE 这种方式。

密码授权模式

    Resource Owner Password Credentials Flow (来自 RFC 6749)

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

密码授权是指用户将自己的账号密码提供给客户端,由客户端向服务提供方获取授权,这种方式的要求是客户端不得存储用户的账号密码,所以只能在用户对于客户端高度信任的情况下才能采用这种方式。

但是所有的客户端中,对用户都应当是不信任的,所以才需要账号密码来验证用户身份;用户对客户端也不应当是完全信任的,对于许多敏感信息都要确认其用途才会提供,除非该客户端是用户的产品。
用户随意提供账号密码带来的风险从我们在社会上看到的例子都可以想象得出,虽然密码授权模式的确是 OAuth 2.0 支持的一种授权模式,但在这种相互不信任的环境下,它往往不会被使用。

客户端凭证模式

    Client Credentials Flow (来自 RFC 6749)

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

客户端凭证模式并不是用来解决 OAuth 要解决的问题的,OAuth 是允许用户让第三方应用获取该用户在某一网站存储的私密资源的协议,但是这一模式是由客户端直接向认证服务器请求一个 acces_token,在请求过程中,客户端是以自己的名义,而不是以用户的名义进行请求,并不存在授权问题。

更新令牌

在上面会发现授权码模式获取令牌的时候,会发现响应中有一个 refresh_token(更新令牌)的可选参数,该参数是用于用户的访问令牌过期,可以直接拿这个更新令牌去授权服务器获取一个新的令牌,这样的好处就是令牌更新时,在服务端就可以重新进行授权,不需要用户重新进行授权,用户不会退出登陆状态。

参考资料

  1. OAuth 2.0 — OAuth
  2. RFC 6749 - The OAuth 2.0 Authorization Framework
  3. 理解 OAuth 2.0 - 阮一峰的网络日志
  4. OAuth 2.0 for Native and Mobile Apps
  5. Implement the OAuth 2.0 Authorization Code with PKCE Flow