OAuth 2.0

OAuth2.0 是目前最为流行的用户验证框架,目前来说各种第三方服务都在用这种方法

相比较传统的 Session Cookie 用户验证来说, OAuth 的灵活性更高,而且整个体系更加完善。所以一旦整个系统的验证授权机制都用 OAuth 来实现得话,其可拓展性会非常好。 无论客户端的 angular.js 还是各种移动端的前端,都可以共享同一套验证体系。

言归正传, oauth 有下面几种定义

  1. Client(application, could be third party app)
  2. Resource Owner(user)
  3. User Agent(browser, mobile app etc.)
  4. Auth Server (e.g. Github or sophisticated auth server)
  5. Resource Server (e.g. Github)
  6. HTTP service (e.g. Github)

OAuth 协议图

+--------+                               +---------------+
|        |--(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 ---|               |
+--------+                               +---------------+

总的来说还是不难理解的。

这副图非常好地揭示了用户和各种 server 的交互,但是隐藏了 server 之间的小小的协议

这就要深入 B 这个过程了

客户端模式 (client credentials grant)

这应该是最简单的 auth 模式了, 严格来说这种模式并不是解决 OAuth 要解决的问题。不过作为 OAuth 其中一种子集还是可以的

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

一般地我们看到的只需要一条 api_key 就可以实现的服务就是采用类似于这种模式 原理很简单,在 application 里面硬码一条 secrete 然后就可以轻松去拿东西了

其中每一条给 auth-server 的 request 必须包含下面这些东西

  1. grant_type=client_credentials(必选项目)
  2. scope=what+ever+scope (可选项目)

其中 auth server 可以用任何方法验证用户(Client) 信息,下面会用一个比较简单的类似 用户名 + 密码 的方法

  1. client_id=client_id_in_the_auth_server
  2. client_secrete=secrete_got_from_auth_server

其中给 auth server的 request 必须 是一条 post request 并且在 body 中 包含下面的内容

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

     client_id=Your+Client+Name
     &client_secret=abcdefg
     &grant_type=client_credentials

这时如果一切都正常的话就 auth server 会传回来一条 token 此时 client 终于可以根据不同类型的 token 进行不同的认证方式,然后到 resource server 去拿资源了

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,
       "scope":"some-scope"
     }

授权码模式(authorization code)

这个模式是所有 OAuth 里面最完善的一套流程。使用这个流程的主要是 Facebook, github

+----------+
| 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)

authorization code 这种方式是最完善的 OAuth2 模式,所以也很复杂,不急,慢慢来

A

这一步应该是用户点击了类似 “用Github 登陆” 之类的按钮,这时候 Application(Client) 应该会放一个 307 然后将用户 重定向到 auth server

其中浏览器放出来的 GET request 要有这几个参数

  1. response_type = ‘code’, // 默认就是 ‘code’
  2. cliend_id // 必选项 声明 client 的身份
  3. redirect_uri // 可选项 声明当做完验证之后回到哪里去
  4. scope // 可选项 声明申请什么样的权限
  5. state // 可选项 声明目前 client server 是什么状态, auth server 会将它返回
GET /authorize
    ?response_type=code
    &client_id=the_client_id
    &redirect_uri=http://redirect.uri.com/callback
    &state=the_status_given_before HTTP/1.1
Host: auth.server.com

B

这步很简单了。 一般都是 auth server 告诉用户: 某某网站要某某权限,你给不给。 然后点个对勾或者输入个用户名密码什么的然后就同意了。

C

同意之后 auth server 也放个 307 出来将 用户 redirect 回来。但是这其中又要包含下面这些东西:

HTTP/1.1 302 Found
Location: http://redirect.uri.com/callback
          ?code=some_code_here
          &state=the_status_given_beofore

注意这个时候 Application(Client) 已经从 auth server 拿到了 code 注意这条 code 的有效期一般很短,默认是10分钟到期

D

所以下一步就是 Application(Client) 拿着这条 code 去 auth server 拿 token

这一步也要是一个 post request, 在 body 中必须有这么几个参数

  1. grant_type // 必选项 规定必须是 authorization_code
  2. code // 必选项 刚刚拿回来的code
  3. redirect_uri // 必选项 规定必须跟 A 步骤中的 redirect uri 是同一个
  4. client_id // 必选项
POST /token HTTP/1.1
Host: auth.server.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=the_client_id
&secrete=the_client_secrete // 登陆方法
&code=some_code_here
&redirect_uri=http://redirect.uri.com/callback

E

如果一切顺利那么 auth server 就应该返回一个 token 了

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

     {
       "access_token":"token_here",
       "token_type":"bearer",
       "expires_in":3600,
       "refresh_token":"refresh_token_here",
       "example_parameter":"example_value",
       "scope": "some-scope some-other-scope"
     }

简化模式 (implicit grant type)

简化模式最厉害的一点就是完全不需要有 Application(Client) 的存在,用纯粹的 javascript 在浏览器端就可以做完整个验证流程。比较典型的应该是 google+

 +----------+
 | 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

第一步发出去的 request 必须包括这么几个东西

  1. response_type // 默认必须是 token
  2. client_id // 必选项
  3. redirect_uri // 可选项
  4. scope // 可选项
  5. state // 可选项
 GET /authorize
     ?response_type=token
     &client_id=s6BhdRkqt3
     &state=xyz
     &redirect_uri=http://app.server.com/cb HTTP/1.1
     Host: auth.server.com

B

用户输入登陆信息,auth server 验证然后回到 application(Client)

C

Auth server 将将用户重新定向回 redirect uri

  1. access_token // 必选项 token 值
  2. token_type // 必选项
  3. expires_in // 过期事件
  4. scope // 可选项
  5. state // 可选项
 HTTP/1.1 302 Found
 Location: http://app.server.com/cb
           #access_token=token_value
           &state=xyz
           &token_type=example
           &expires_in=3600

D

此时 浏览器收到了token和 C中带有 token 的 location 值。按照 spec 此时浏览器将把用户重定向到不带有 hash 的 URI 中去。

同时 浏览器在本地存储了 token 的值

E

静态网页中有一条将token 传回给 application(Client) 的 js,此时浏览器本地存储了 token 的值,js 会想办法把这条 token 交回给 application

F

然后无论是发起一个 post request 还是用 xhr,浏览器将 token 交回给了 application(Client) 验证结束

最后。相比较最完善的 code 模式来说,简化模式是明显有一些有点的

  1. 不需要跟 application server 沟通,直接去 auth server 拿 token
  2. 所以可移植性更好

但是缺点同样明显。主要有几点

  1. token存在浏览器客户端中很容易就被人给顺走了
  2. 在 spec 中没有提到关于 refresh token 的事。

密码模式 (Resource Owner Password Credentials Grant)

这个模式简单讲就是用户直接把 用户名密码给 Application(Client) 然后 Application(Client) 负责将这堆东西给 resource server。

使用这个模式的应用基本没有。 只有在很特殊的情况下,比如前三种模式都不灵的时候才会 fallback 到这种那个模式中。

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

步骤很简单

总的来说相比较前两种 password 模式就很简单了,唯一要注意的就是在 B 中 有几个参数是有规范的

  1. grant_type // 必选项 password
  2. username // 必选项
  3. password // 必选项
  4. scope // 可选项

传回来的东西跟 client credentials 和 authorization code 两种模式一样

Refresh Token

前面一直看到一个概念就是 refresh token, 那么这个 特殊的 token 到底在干什么卵用? 之前我们看到了各种 json 中都有 specific 一个过期时间。 假设当前的 token存在浏览器客户端中很容易就被人给顺走了被黑客给顺走了,(http 截取什么的) 当这个时限到了之后那黑客就真的黑了。

我们知道因为拿 token 的时候 Application(Client) 是要向 authserver 表明自己的身份的,所以在使用 refresh token 的时候相当于用 application 的身份担保了这个 reset 的权威性。这时候除非黑客盗取了 Application 在 auth server 那边的 credential

所以正常来件应该是每天做一次 refresh 这样的安全性会高不少

   POST /token HTTP/1.1
     Host: auth.server.com
     Authorization: Bearer bearer_token_here
     Content-Type: application/x-www-form-urlencoded

     grant_type=refresh_token
     &refresh_token=refresh_token_value

这里注意的就是

  1. grant_type // 必选 refresh_token
  2. refresh_token // 必选 token 的值

参考:

happy coding, may the code will always be with you~