OAuth2.0 Web Application Flowの実装

| コメントをどうぞ

はじめに

はじめまして。infoScoop OpenSource開発のお手伝いをさせていただいておりますDiceK Mikamiと申します。
開発者ブログでは、ウェブ/ローカル問わず簡単な技術Tipsなどに関して投稿させていただこうかと考えております。
とは言いましても、私自身まだまだ若輩ゆえ誤り等々があるかと思いますので、
その際には皆様からの生暖かいご指摘/アドバイスなどいただければ幸いです。
今後ともよろしくお願いいたします。

OAuth2.0認証

OAuth2.0は、OAuth1.0Aにて不便と思われていたいくつかの仕様(例えば、署名など)を改善し、
より簡単にサービス間の連携を行うための認証機構です。
OAuth2.0による認証はGoogleやFacebookをはじめ、いくつかのサービスですでに提供されるようになりましたが、
仕様自体はまだドラフトの段階であり、未だ策定作業が続いていると言うのが現状です。

The OAuth 2.0 Authorization Protocol draft-ietf-oauth-v2-23

ですので、現状でサーバーサイドにてOAuth2.0実装を行う場合、
策定されているドラフトを元にするか、あるいは他のサービスとの連携をふまえて実装する必要性があります。
今回は実際にinfoScoop OpenSource V3.0にて実装したコードをパクって経験を活かして、
サーバーサイドでOAuth2.0認証をしてみることにします。

OAuth2.0 Web Application Flow

Web Application Flowでは、下図のような流れで認証し、データを取得することになります。

図では上方向から下方向にかけて、状態が遷移していきます。
Aはアプリケーション側のスタートポイントクラスになります。
ここで認証サーバーに対して認可コードを要求します。
認可コードとはOAuth1.0Aにおけるリクエストトークンに類似したものですが、
基本的にワンタイムの使用となります。
認可コードを得るためには、ユーザーは認証サーバーに対してログインする必要があります。
また、この際にアクセスするAPIに対しての権限を許可するかどうかの応答も行う必要があります。
Bは認証サーバー側のAuthorizationクラスになります。

認可コードはアプリケーションのコールバッククラスに返却されます。(C)
コールバックURLは認証サーバー側に登録しておく必要があります。
Google、Facebookで確認したところ適当なURLではダメでした。
コールバッククラスにて、認可コードを使ってアクセストークンを要求します。
取得先は認証サーバーごとに提示されているAccessTokenクラスになります。(D)
アクセストークンを返却してくる際に、認証サーバーはアクセストークンの持続時間(expires)とリフレッシュトークンも同時に返却してきます。
OAuth2.0ではアクセストークンに持続時間が付与されており、
時間が過ぎてしまったトークンは無効化されてしまいます。
この際、無効化されたトークンを再発行するために利用するのがリフレッシュトークンとなります。
注釈:現状、Facebookではリフレッシュトークンを返却しません。サービスによって実装はまちまちです。

サンプルコード

サンプルでは、Facebookからフレンド一覧を取得するコードをもとに実際の挙動を解説します。
今回のサンプルでは、スタートポイントとなるOAuth2Sample.javaと
コールバッククラスとなるOAuth2Callback.javaの2つのサーブレットを使用します。
(サンプルは要点だけを抜粋して記述してあります。また、実際にOAuth2.0を試すためには各サービス毎にアプリケーションの登録を行う必要があります)

// OAuth2Sample.java
 public void doGet(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
  // 認可コード取得URLを返却------------------(a)
  res.setContentType("text/html; charset=UTF-8");
  PrintWriter out = res.getWriter();
  out.println(createHTML());
  out.close();
 }
 
 private String createHTML(){
  StringBuffer sb = new StringBuffer();
  sb.append("<html><head><title>OAuth2サンプル</title></head><body>");
  sb.append("<a href='"+AUTH_URL);
  sb.append("?client_id="+APP_ID);
  sb.append("&redirect_uri="+REDIRECT_URL);
  sb.append("&response_type=code");
  sb.append("&state=xxx");
  sb.append("&scope="+"user_about_me,offline_access");
  sb.append("'>認可コードを取得します。");
  sb.append("</a></body></html>");
  return (new String(sb));
 }

(a)より認可コードを取得するためのアドレスをクライアントに返却する処理を記述しています。
認可コード取得先にはGETリクエストする必要がありますので、そのままアドレス返却します。
認可コード取得先にリクエストするためのパラメータは以下になります。

GET https://www.facebook.com/dialog/oauth
client_id: <サービスから発行されるAPP ID>
redirect_uri: <サービスに登録したコールバックURL>
response_type: "code"
state: <コールバックURLに渡したい値を記述>
scope: "user_about_me,offline_access"

Facebookではリフレッシュトークンが実装されていません。
そのためアクセストークンを永続化させるためにscopeのパラメータとして「offline_access」を付加しています。
扱えるスコープの情報に関しては、各サービスのドキュメントを参考にしてください。

facebook Developer(英語)

リクエストを行うと、以下のような形で値が返却されます。

// OAuth2Callback.java
 public void doGet(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
  // 認可コード解析------------------(b)
  HashMap<String,String> authCodeMap = parseRequest(req);
  HttpClient client = new DefaultHttpClient();
  
  // アクセストークン取得------------------(c)
  HttpPost httpPost = new HttpPost(TOKEN_URL);
  List<NameValuePair> postParams = new ArrayList<NameValuePair>();
  postParams.add(new BasicNameValuePair("code",authCodeMap.get("code")));
  postParams.add(new BasicNameValuePair("client_id",APP_ID));
  postParams.add(new BasicNameValuePair("client_secret",APP_SECRET));
  postParams.add(new BasicNameValuePair("redirect_uri",REDIRECT_URL));
  postParams.add(new BasicNameValuePair("grant_type","authorization_code"));
  httpPost.setEntity(new UrlEncodedFormEntity(postParams, HTTP.UTF_8));
  HttpResponse authTokenResponse = client.execute(httpPost);
  
  // アクセストークン解析------------------(d)
  HttpEntity httpEntity = authTokenResponse.getEntity();
  HashMap<String,String> accessTokenMap = parseQuery(EntityUtils.toString(httpEntity));
  
  // 実データ取得------------------(e)
  HttpGet httpGet
       = new HttpGet(ACCESS_URL+"?access_token="+accessTokenMap.get("access_token"));)
  HttpResponse dataResponse = client.execute(httpGet);
  HttpEntity dataEntity = dataResponse.getEntity();
  
  res.setContentType("text/html; charset=UTF-8");
  PrintWriter out = res.getWriter();
  out.println(EntityUtils.toString(dataEntity));
  out.close();
 }

認可コードは登録したコールバックURLに返却されてきます。
(b)では返却された認可コードを解析し、ハッシュマップに登録しています。

認可コードが取得できたら、認可コードを利用してアクセストークンを取得します。
アクセストークン取得先には、POSTリクエストする必要があります。
(サーバーサイドからHTTPアクセスできるようにApache commons HTTP Clientを利用しています)
リクエストの際のパラメータは以下になります。

POST https://graph.facebook.com/oauth/access_token
code: <返却された認可コード>
client_id: <サービスから発行されるAPP ID>
client_secret: <サービスから発行されるAPP SECRET>
redirect_uri: <サービスに登録したコールバックURL>
grant_type: "authorization_code"

アクセストークンリクエスト時の返却値は以下のようになります。
今回は「offline_access」スコープを付与しているので、expiresは返却されていません。


最後に返却されてきたアクセストークンを利用して、データを取得しています。
アクセストークンをサービスに渡す方法は複数提案されておりますが、
今回はGETアクセスのパラメータとしてトークンを渡しています。

GET https://graph.facebook.com/me/friends
access_token: <返却されたアクセストークン>

結果として以下のデータが取得できました。
個人情報が含まれるためモザイクを入れておりますが、
data属性にフレンド情報が入っていることが確認できると思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>