Oauth2 Authorization Code Grant模式分析
OAuth2简介
OAuth2是一个将有限的HTTP服务访问权限提供给第三方应用的框架。OAuth2用于替换OAuth,OAuth2与OAuth不兼容,但是两者可以在一个系统中共存。
OAuth2的角色
-
资源拥有者 可以授予第三方服务访问受保护资源的权限。如果资源拥有者为个人,那么一般是指最终使用者。
-
资源服务器 受保护资源所在的服务器。
-
客户端 访问资源服务器的一个应用。只有在得到资源拥有者的授权后,客户端才能访问受保护的资源。
-
授权服务器 访问令牌提供服务器。在资源拥有者授权客户端后,授权服务器会提供一个访问令牌给客户端,客户端后续访问资源服务器都要提供这个令牌。
比如有这样的场景,老王想要访问某论坛X,但是他又没有这个论坛的账号,而且他不想注册这个论坛的账号。这个论坛提供了通过Google账号访问的功能。于是老王就可以通过授权论坛X来获得他在谷歌的用户名并以此访问论坛X。
在这个场景中资源拥有者是老王,而他拥有的资源就是他在Google的账号。资源服务器就是Google的用户服务器。客户端就是论坛X的服务器。授权服务器就是Google的授权服务器。
OAuth2 Authorization Code Grant模式流程
OAuth2提供了4种获取授权的模式,分别是Authorization Code Grant,Implicit Grant,Resource Owner Password Credentials Grant,Client Credentials Grant。这篇文章只分析第一种模式。 Code Grant是一个基于HTTP重定向的流程,客户端(应用程序)需要与用户的浏览器进行交互。从RFC6749直接搬过来的流程图如下。
+----------+
| 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)
各步骤的说明如下,为了方便理解上图中的User Agent使用浏览器代替。
- A. 用户需要访问受保护资源的时候,客户端给浏览器发送一个重定向请求,重定向请求的query parameter包括一个认证成功后的回调地址,客户端的唯一标识符(client_id), 请求范围(request scope), 本地状态(local state)。浏览器将页面跳转到授权服务器的登录页面。
- B. 用户登录以确保该用户是资源拥有者。用户同意客户端所请求的权限(访问某个或某些资源)。
- C. 授权服务器返回一个302请求给浏览器,请求的query parameter包括Authorization Code,State。302的location是在步骤A中设置的客户端的回调地址。浏览器跳转到location。
- D. 客户端使用C中获取的Authorization Code,发送请求给授权服务器以获取访问令牌和更新令牌。请求参数中包括跟A中一样的回调地址,并提供客户端的client_id和client_secret。
- E. 授权服务器接到请求后会认证客户端和步骤A中的客户端是同一个并且密码正确,检查Code,还要确保回调地址和C中的回调地址是同一个。如果所有检查都通过,授权服务器返回访问令牌和更新令牌(可选)。
我对这个流程比较困惑的是为什么授权服务器不在步骤C直接在回调地址里包含两个令牌作为query parameter,而是要在E中才真正返回。Google了下我的疑问,在Stack Overflow找到了一个相同的问题,得分最高的回答解开了这个谜团。虽然OAuth2服务器必须使用HTTPS协议访问,但是浏览器和客户端之间无法保证是使用HTTPS的,因此如果在步骤C中返回令牌就很有可能被截获。因此在步骤C中只返回一个Authorization Code。即使这个Code和client_id被截获了,由于截获者没有client_secret,他也无法完成步骤E中的客户端认证,从而无法获取令牌。而任何一方跟授权服务器之间的访问由于使用了HTTPS而无法被截获。
使用go oauth2 package获取github用户的用户名和ID
下面通过一个golang的例子说明Authorization Code Grant的各个过程。要运行下面的代码需要有一个Github的账户,并在账户的Settings->Developer settings
中新建一个OAuth App。有了client_id和client_secret可以替换代码中相应的变量。Authorization callback URL这个字段一定要填写正确,这个就是获得code和令牌后的回调地址,Github会验证回调地址是否一致。
各个步骤在代码中都有注释,步骤B不在其中,是因为这个是Github跟用户之间的交互,客户端不需要干预。各个步骤还是非常清晰的,看代码应该就能明白,不多做说明了。
程序编译运行后访问服务器的9191端口就可以看到结果了。
package main
import (
"context"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
func Home(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello oauth2"))
}
func Begin(c *oauth2.Config) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// get authorization redirect url
redirectURL := c.AuthCodeURL("state")
// Step A:
http.Redirect(w, r, redirectURL, http.StatusFound)
}
}
func End(c *oauth2.Config) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Step C: get authorization code
queryCode := r.URL.Query().Get("code")
if queryCode == "" {
http.Error(w, "empty code", http.StatusBadRequest)
return
}
ctx := context.Background()
// Step D & E: get token
token, err := c.Exchange(ctx, queryCode)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !token.Valid() {
http.Error(w, "invalid token", http.StatusBadRequest)
return
}
// access resource after we get token
url := "https://api.github.com/user"
// pass token to client
client := c.Client(ctx, token)
response, err := client.Get(url)
if err != nil {
http.Error(w, "get user info from github failed", http.StatusBadRequest)
return
}
userInfo, err := ioutil.ReadAll(response.Body)
if err != nil {
http.Error(w, "read body failed", http.StatusBadRequest)
}
response.Body.Close()
w.Write(userInfo)
}
}
func main() {
// configure oauth2 client setting
c := &oauth2.Config{
ClientID: "your_client_id",
ClientSecret: "your_client_secret",
RedirectURL: "http://your_host_name:9191/end", // match Authorization callback URL setting in github application
Endpoint: github.Endpoint,
Scopes: []string{},
}
http.HandleFunc("/", Home)
http.HandleFunc("/end", End(c))
http.HandleFunc("/begin", Begin(c))
http.ListenAndServe(":9191", nil)
}