2015年4月22日 星期三

在網站應用程式中取得 Google OAuth 2.0 授權

Google OAuth 2.0 伺服器支援 Web 程式存取,如 ASP.NET, PHP, Java, Python 等。 只要程式端握有存取憑證,不管使用者是否有在線上,都可以直接存取 Google API 。

架構概說

在 Google 的 Using OAuth 2.0 for Web Server Applications 文件中,有底下這張循序圖,說明了 OAuth2 使用流程。

步驟說明:

  • 將 user 導向 Google 的 consent 頁面以便向 OAuth Server 要求 Token 。
  • 使用者同意授權。
  • Google 回傳一個授權碼到指定的端點位址
  • 在該端點位址中,利用這個授權碼(Code)與 Google 交換存取憑證(Token)。
  • 利用這個存取憑證(Token)叫用 Google API。

申請「網路應用程式」憑證

存取 Google API 必須先至 Google Developers Console 申請應用程式憑證。

在上面這個設定畫面中,有個項目叫授權的重新導向URI,指的就是用來接收處理 authorization code 的網址。

取得 Access Token

要取得 Access Token ,你可以依底下步驟設計,更詳細的說明,請參考文件:https://developers.google.com/identity/protocols/OAuth2WebServer#formingtheurl

1. 取得使用者授權

當 APP 要存取 Google 的資源之前,必須先取得使用者的同意授權,你可以將 Scope, Client Id 送至 https://accounts.google.com/o/oauth2/auth 要求使用者授權。 範例說明:

送出需求

https://accounts.google.com/o/oauth2/auth?
scope={0}&
state={1}&
redirect_uri={2}&
response_type=code&
client_id={3}&
approval_prompt=force 
  • scope:授權範圍。
  • state:可任意附加的字串,Server會將此參數值回傳到 RedirectUri 那個網址。
  • redirect_uri:給 Google OAuth Server 回傳 Authrization Code 的網址。
  • response_type:回傳的型別 Code 或 Token,網站類型應用程式不建議使用Token。
  • client_id:。申請的Client ID

程式範例:

string scope = "https://www.googleapis.com/auth/blogger";

    string oauthURL = "https://accounts.google.com/o/oauth2/auth?" +
                    "scope={0}&state={1}&redirect_uri={2}&response_type=code&client_id={3}&approval_prompt=auto";

    string state = "bloglist";

    oauthURL = string.Format(oauthURL,
                    HttpUtility.HtmlDecode(scope),
                    HttpUtility.HtmlDecode(state),
                    HttpUtility.HtmlDecode(redirect_uri),
                    HttpUtility.HtmlDecode(client_id));

    HttpContext.Current.Response.Redirect(oauthURL);

執行結果所開啟的授權頁面:

處理回應

如果使用者在 consent 頁面中按下了同意授權按鈕,Google 將會回傳一個 authorization code 到指定的網址,如下範例:

http://localhost:7003/OAuth/oauth.aspx?code=4/eEMTPZGz6SDgON2SalG0x9TcGkWh8CtkUocmPljhcHM.UunbPgwwZF8ZgrKXntQAax10lkHImQI&state=bloglist

若使用者按下不同意授權,Google 將回傳以下資料:

http://localhost:7003/OAuth/oauth.aspx?error=access_denied&state=bloglist

所以這個 redirect_uri 就是用來處理回應的網頁,你可以在這個端點中提取 authorization code ,並做適當的處理。

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        string code = Request["code"];
        string state = Request["state"];

        if (code != null)
        {
            switch (state)
            {
                case "bloglist":
                    GetBloggerList(code);
                    break;
                case "user":
                    GetUserProfile(code);
                    break;
            }
        }
        else
        {
            string error = Request["error"];
            Response.Write(error);
            Response.End();
        }
    }
}

2. 取得存取憑證(Access Token)

前面在取得使用者授權的網址中有個參數叫 redirect_uri ,這個參數值必須等同向 Google 申請網路應用程式憑證時所填寫的網址。 你可以在這個網址中取得授權碼,再和 Google 交換存取憑證(Access Token)。

送出需求

在取得 authorization code 之後,你可以送出以下需求,以取得存取憑證:

POST https://www.googleapis.com/oauth2/v3/token
Content-Type: application/x-www-form-urlencoded

code={authorization code}&
client_id={your client_id}&
client_secret={your client_secret}&
redirect_uri={your redirect_uri}&
grant_type=authorization_code

底下是 ASP.NET 的程式碼範例,示範如何送出上述的需求內容:

protected void Page_Load(object sender, EventArgs e)
    {
        string code = Request["code"];      //Google傳過來的授權碼
        string state = Request["state"];    //傳送自訂參數值

        if (code != null)
        {
            string useremail = ConfigurationManager.AppSettings["UserEmail"];
            string appname = ConfigurationManager.AppSettings["AppName"];

            Google_WebClient oauth = new Google_WebClient(useremail, state, null, appname);
            oauth.Token = oauth.RequestToken(code);
            oauth.SaveToken();
        }
        else
        {
            Console.WriteLine(Request["error"]);
        }
        this.ClientScript.RegisterClientScriptBlock(this.GetType(), "Close", "window.close()", true);
    }
public class Google_WebClient
{
    public TokenResponse RequestToken(string code)
    {
        string tokenUrl = string.Format("https://www.googleapis.com/oauth2/v3/token");

        string queryString = @"code={0}&client_id={1}&client_secret={2}&redirect_uri={3}&grant_type=authorization_code";
        string postContent = string.Format(queryString,
                                    HttpUtility.HtmlEncode(code),
                                    HttpUtility.HtmlEncode(client_id),
                                    HttpUtility.HtmlEncode(client_secret),
                                    HttpUtility.HtmlEncode(redirect_uri));

        HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(tokenUrl);
        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        using (var sw = new StreamWriter(request.GetRequestStream()))
        {
            sw.Write(postContent);
        }

        var result = "";
        using (var response = request.GetResponse())
        {
            using (var sr = new StreamReader(response.GetResponseStream()))
            {
                result = sr.ReadToEnd();
            }
        }

        GoogleOAuthToken token = JsonConvert.DeserializeObject<GoogleOAuthToken>(result);
        return token;
    }
}

處理回應

若需求成功,將回應一個 JSON 格式的 Access Token ,如下:

{
  "access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
  "expires_in":3920,
  "token_type":"Bearer"
}

3. 取得離線存取憑證(Offline Access Token)

Access Token 有一定的時效性,且時效通常不會太長,如果超過有效期限,APP 就必須要求使用者重新 Consent 以取得 Access Token 。 可是如果使用者不在線上, APP 又必須存取 API ,這時候就可以使用「離線存取」,它的目的就是不須要使用者重新 Consent 就可以獲得新的 Access Token ,例如 APP 想在離鋒時段下載資料以進行備份。

如果 APP 需要離線存取,只須要在授權 API 中,加入 access_type = offline 參數即可。

https://accounts.google.com/o/oauth2/auth?
scope={0}&
state={1}&
redirect_uri={2}&
response_type=code&
client_id={3}&
approval_prompt=force&
access_type=offline

在使用者同意授權後,OAuth Server 同樣的會回傳一個 authorization code 到 redirect_uri ,你就可以拿它去取得 Access Token 。

POST https://www.googleapis.com/oauth2/v3/token
Content-Type: application/x-www-form-urlencoded

code={authorization code}&
client_id={your client_id}&
client_secret={your client_secret}&
redirect_uri={your redirect_uri}&
grant_type=authorization_code

不過這時候取得的 Token 資料中,會多一個 refresh token 。

{
  "access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
  "expires_in":3920,
  "token_type":"Bearer"
  "refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}

若你用這個方式取得 Refresh Token ,你必須將它儲存起來,之後才能利用它與 OAuth Server 要求新的 Access Token 。

4. 使用 Refresh Token 更新 Access Token

當 Access Token 過期,而你又握有 Refresh Token 時,你就可以直接透過它取得新的 Access Token ,無須要求使用者再次 Consent 。

POST https://www.googleapis.com/oauth2/v3/token
Content-Type: application/x-www-form-urlencoded

refresh_token={refresh_token}&
client_id={your client_id}&
client_secret={your client_secret}&
grant_type=refresh_token

使用 refresh token 重置 token 時,回傳資料中將不會再包含 refresh token ,例如:

{
  "access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
  "expires_in":3920,
  "token_type":"Bearer"
}

雖然新取得的 token 不含 refresh token ,但是原先的 refresh token 還是有效的,等下次 token 過期時,你還是可以直接用舊有的 refresh token 去交換新的 token 。

5. 撤銷使用者授權

若使用者想要撤銷授權,他可以自行到 https://accounts.google.com/b/0/IssuedAuthSubTokens 網站撤銷,除此之外,也可以透過程式來撤銷。

https://accounts.google.com/o/oauth2/revoke?token={0}
  • token:這個參數值,可以帶入 access token 或 refresh token 。

如果撤銷成功,回應狀態碼 200 ;撤銷不成功,則回應狀態碼 400 。

叫用 Google API

在 Google 眾多的服務中,若要透過 API 存取資源,如果只是讀取較一般性的資料,通常無需授權,或者只需要使用 ApiKey 即可。 只有存取較隱私性或者需要更新到資料時,才會要求一定要使用 access token 。 不過,實際上的要求還是要參考該 API 的規定,底下這個例子,我們示範如何叫用 Google Blogger API 以取得該使用者的所有 Blog 清單。 參考文件:Retrieving a user's blogs

例如取得使用者 Blog 清單的文件記載:

該需求中說明這個取得 blog 清單的 API 端點位址,在送出去的 request 中,也必須在 Header 中提供 Authorization 資訊,內容就是 access token 的值。 另外送給 Google 的 Authorization 資訊,都必須標註它是一個 Bearer 型式的 Token 。

string uri = "https://www.googleapis.com/blogger/v3/users/self/blogs";
    string result = "";

    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
    request.Method = "GET";
    request.Headers.Add(HttpRequestHeader.AcceptLanguage, "zh-tw");
    request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + access_token);

    using (var response = request.GetResponse())
    {
        using (StreamReader sr = new StreamReader(response.GetResponseStream()))
        {
            result = sr.ReadToEnd();
        }
    }

    BlogList bloglist = JsonConvert.DeserializeAnonymousType(result, new BlogList());
    foreach (var blog in bloglist.Items)
    {
        Response.Write(blog.Name + "</br>");
    }

3 則留言:

  1. 不好意思,請問一下用google登入後,我該怎麼取德G+個人大頭貼呢?

    回覆刪除
    回覆
    1. 要取得G+個人資料, scope 設定必須包含 "profile"
      等使用者授權後, 利用取得的 access_token 對 "https://www.googleapis.com/oauth2/v1/userinfo" 送出 request
      就可以得到 G+ 公開的資訊, 包含大頭貼的網址.

      刪除
  2. VITO大大~請問一下~申請OAuth一定要用gmail帳號嗎?可否用公司的mail?

    回覆刪除