Google APIではOAuthのバージョン2.0(推奨)と1.0が利用可能となっている。OAuthとは他webサービスで公開されている機能を利用するための認証用プロトコルである。OAuthに対応しているサービスとしては、GoogleやFacebookの一部webサービス等が該当する。スマートフォン・アプリなどで『このアプリケーションが次の機能へのアクセスを求めています。…』と表示されることがあると思うが、あれのweb版である。
今回はライブラリを利用せずにGoogleでOAuth2.0を利用する方法を確認する。これはライブラリを利用してしまうとプロトコル本来の動作イメージが分からなくなってしまうからであり、実際にwebアプリを公開する場合にはライブラリを利用することを推奨する。なぜならOAuthで扱う情報は認証情報であり、暗号化せずに通信してしまう等による情報流出リスクを避けるためである。ライブラリの利用方法は、また別の機会に確認する。
■ OAuthとは?
OAuthとは他webサービスで公開されている機能を利用するための認証用プロトコルである。例えばGoogleであればG-mailやyoutubeなど多数のサービスがOAuthに対応しており、サービス利用者(プログラマ)がサービスの一部を利用できるようになっている。このとき、利用できるサービスの範囲をスコープという。
OAuth利用の流れ
OAuthの認証の関係者は、ユーザ・サービス利用者・サービス提供者の3者である。Google App EngineからGoogleサービスを利用する場合を考えると、ユーザがブラウザ(GAEを使う人)、サービス利用者sがGoogle App Engine上のプログラム、サービス提供者が利用するGoogle APIサービスである。OAuth利用時の3者の動きを以下に示す。
図:OAuthの動作を表すシーケンス図
まずユーザはGoogle App Engine上のwebページにアクセスする(①)。ユーザがGoogle APIの機能を使うページにアクセスした場合、Google App Engineはページを表示する前にGoogle API上でユーザ認証を行ってもらうため認証ページへとリダイレクトさせる(②)。Google APIの認証ページに飛ばされたユーザは、Googleアカウントでログインを求めらる(③)。ログインに成功すると、Google APIはGAEプログラムにより一部機能(およびGoogle API上の情報)の利用証人を求めている旨を表示し、ユーザに承認するか確認する。
ユーザが承認すると、Google APIはGAEプログラムに対して「authorization code」と呼ばれるワンタイム・パスを発行する(④)。「authorization code」を受け取ったGoogle App Engineはそのコードを元に、Google APIから「access code」をはじめとする幾つかのトークンを取得する(⑤)。Google App EngineがGoogle APIを利用する場合には、利用するAPIのパラメータに加え「access code」を渡すことでAPIの利用が可能となる(⑥)。Google App EngineはGoogle API等を利用しながらブラウザに表示する画面を作成し、結果の画面がユーザに表示される(⑦)。
上記の流れを見れば分かるとおり、OAuthの利点としてはサービス利用者に対してIDやパスワードが知らされない点があげられる。「access code」という一時パスワード(サーブレットで利用するSession IDのようなもの)を利用することにより、OAuthは一時的にAPIを利用する権限のみを提供することを実現しているのである。
■ GoogleサービスでOAuthを利用する前準備
ではGoogle App Engine上からOAuth認証によりGoogle APIを利用する方法を見ていく。
Google Developers Consoleでの作業
まず、Google APIを利用するため、APIのクライアントIDとシークレット(パスワードのようなもの)を取得する。IDとシークレットは
Google Developers Consoleの「認証情報」から認証情報を作成することにより取得できる。認証情報の作成では「OAuthクライアントID」を選択する。
認証情報が作成されるとクライアントID、クライアント・シークレットが自動作成される。このIDに紐づく名前と、このクライアントIDを利用した場合に「authorization code」を返すリダイレクト先のURLを設定するとConsole上の設定は終了である。
利用するGoogle APIのサービスURLとスコープを取得
例えばGMailを利用したい場合など、GAEがGoogle APIに利用したいサービスを指定する際にはサービスのURLとスコープ(利用の範囲)が必要である。これらはOAuthのデモンストレーションサイトである「
OAuth 2.0 PlayGround」で確認できる。PlayGroundは上記で説明したOAuthプロトコルの流れを1つづつ順を追って確認するためのデモサイトである。
PlayGroundの利用方法の概要を確認する。まずはStep1で利用可能なスコープ名の一覧が表示されている。GAEからGoogle APIを利用する場合、ここに表示されているスコープ名が利用できる。一覧の中から利用したいサービス(スコープ名)を一覧から指定して「Authorize APIs」ボタンを押下すると以下のようなGoogle APIのユーザ認証確認画面にリダイレクトされる。「許可」ボタンを押下する。
次に、Step2でGoogle APIから受け取った「Authorization code」が表示されるため、「Exchange authorization code for tokens」ボタンを押下。すると「Refresh token」「Access token」が取得される。次にStep3に移り「List possible operations」ボタンを押下すると利用可能なサービスのURIが一覧表示される。GAEからGoogle APIを利用する際にも、このサービスURLを利用する。一覧から利用するサービスURIを選択すると、Request URIの項目に同URLが表示される。最後に「Send the request」ボタンを押下すると、取得した「Access token」を利用してGoogle APIサービスの要求が発生し、結果が画面右側に表示される。
■ サンプル・プログラム
以下にGoogle App Engine / JavaでOAuth2.0を利用するサンプル・プログラムを示す。サンプルでは「http://localhost:8888/OAuth2Test」にアクセスすると、OAuth2.0認証を使ってGoogleのユーザ情報取得サービスにアクセス・結果を出力する。なお、プログラム作成にあたりkimukou_26さんのサイト(
*4)を参考にさせていただいた。
また、ユーザ情報を取得するために利用するスコープと利用サービスは以下のとおりである。
項目 |
内容 |
スコープ |
https://www.googleapis.com/auth/userinfo.profile |
利用サービス |
ユーザ情報取得
https://www.googleapis.com/oauth2/v2/userinfo |
◇サンプルコード
<!-- Javaクラスの定義 -->
<servlet>
<servlet-name>OAuth2Test</servlet-name>
<servlet-class>com.appspot.karu_lab.TestOAuth2Servlet</servlet-class>
</servlet>
<!-- URLマッピング -->
<servlet-mapping>
<servlet-name>OAuth2Test</servlet-name>
<url-pattern>/OAuth2Test</url-pattern>
</servlet-mapping>
package com.appspot.karu_lab;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* OAuth 2.0を利用して、Googleサービスの情報を取得
* @author karura
*
*/
public class TestOAuth2Servlet extends HttpServlet
{
// 定数
static final String ENDPOINT = "https://accounts.google.com/o/oauth2"; // GoogleでOAuth 2.0認証を行うページのURL
static final String CLIENT_ID = "××××××××××××××××××××××××××××××××××××"; // OAuth2クライアントID。Google Developers consoleに表示されている値
static final String CLIENT_SECRET = "××××××××××××"; // OAuth2クライアント・シークレット(暗号化のソルトにあたるもの)。Google Developers consoleに表示されている値
static final String REDIRECT_URI = "http://localhost:8888/OAuth2Test"; // リダイレクト先URL。Google Developers consoleに表示されている値。
static final String SCOPES = "https://www.googleapis.com/auth/userinfo.profile"; // スコープ(使用権限を求めるアプリ。複数の場合はカンマで区切る)
static final String SERVICE_URI = "https://www.googleapis.com/oauth2/v2/userinfo"; // OAuth2認証によって利用するサービスのURL
/**
* GETパラメータによるURL起動
*/
public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws IOException
{
// GETパラメータの取得
String state = req.getParameter( "state" ); // 現在の状態
String authorizationCode = req.getParameter( "code" ); // 「authorization code」の取得
String error = req.getParameter( "error" ); // エラー
// エラーがある場合はエラー画面を表示
if( error != null )
{
// エラー表示
resp.setContentType( "text/plain" );
resp.getWriter().println( String.format( "error : %s" , error ) );
}
// 正常処理
try {
// OAuth 2.0 認証処理
if( state == null )
{
// OAuth 2.0 によるユーザ認証が行われていない場合は
// まずユーザ認証を行う
StringBuilder b = new StringBuilder();
b.append( "response_type=code" );
b.append( "&client_id=" ).append( URLEncoder.encode( CLIENT_ID , "utf-8" ) );
b.append( "&redirect_uri=" ).append( URLEncoder.encode( REDIRECT_URI , "utf-8" ) );
b.append( "&scope=" ).append( URLEncoder.encode( SCOPES , "utf-8" ) );
b.append( "&state=dummy" );
// Googleの認証サイトにリダイレクト
resp.sendRedirect( ENDPOINT + "/auth?" + b.toString() );
}else if( state.equals( "dummy" ) )
{
// OAuth 2.0 によるユーザ認証が行われた場合は
// Googleのサービスにアクセスする
// authorization codeを元に各種トークンを取得する
Map<String,String> tokenMap = getTokenMap( authorizationCode);
// accessトークンを利用して、Googleサービスからユーザ情報を取得
Map<String,String> jsonMap = getJSONMap( SERVICE_URI , tokenMap.get( "access_token" ) );
// POSTパラメータを書き込み
resp.setContentType( "text/plain" );
PrintWriter out = resp.getWriter();
out.println( "print your userinfo." );
out.println( "" );
// ユーザ情報をすべて出力
Iterator<String> ite = jsonMap.keySet().iterator();
while( ite.hasNext() )
{
// キー取得
String key = ite.next();
// 値
String value = jsonMap.get( key );
// 出力
out.println( String.format( "%s = %s" , key , value ) );
}
}else{
// 不明なステータスとなった場合はエラー表示
resp.setContentType( "text/plain" );
resp.getWriter().println( "state is not found!" );
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* OAuth 2.0 のauthorization codeを利用して、各種トークンを作成する。
* 結果のJSONデータは連想配列(Map)として返す
* @param authorizationCode
* @return
* @throws Exception
*/
public Map<String,String> getTokenMap( String authorizationCode ) throws Exception
{
// 戻り値変数を準備
Map<String,String> jsonMap = null;
// パラメータを組み立てる
StringBuilder b = new StringBuilder();
b.append("code=").append(URLEncoder.encode(authorizationCode, "utf-8"));
b.append("&client_id=").append(URLEncoder.encode(CLIENT_ID, "utf-8"));
b.append("&client_secret=").append(URLEncoder.encode(CLIENT_SECRET, "utf-8"));
b.append("&redirect_uri=").append(URLEncoder.encode(REDIRECT_URI, "utf-8"));
b.append("&grant_type=authorization_code");
byte[] payload = b.toString().getBytes();
// POST メソッドでリクエストする
HttpURLConnection c = (HttpURLConnection) new URL(ENDPOINT + "/token").openConnection();
c.setRequestMethod("POST");
c.setDoOutput(true);
c.setRequestProperty("Content-Length", String.valueOf(payload.length));
c.getOutputStream().write(payload);
c.getOutputStream().flush();
// トークン類が入ったレスポンスボディを解析する
StringBuilder json = new StringBuilder();
BufferedReader reader = new BufferedReader( new InputStreamReader( c.getInputStream() ) );
String line = null;
while ( (line = reader.readLine() ) != null)
{
json.append(line).append("\n");
}
reader.close();
c.disconnect();
// 文字列をハッシュマップに変換
jsonMap = cnvJSONToMap( json.toString() );
return jsonMap;
}
/**
* OAuth 2.0 のaccessTokenを利用して、サービスにアクセスする。
* 結果のJSONデータは連想配列(Map)として返す
* @param url
* @param accessToken
* @return
* @throws Exception
*/
public Map<String,String> getJSONMap( String url , String accessToken ) throws Exception
{
// Google API からユーザ情報を取得
HttpURLConnection c = (HttpURLConnection) new URL( url ).openConnection();
c.setRequestMethod("GET");
c.setRequestProperty( "Authorization" , "OAuth " + accessToken );
// トークン類が入ったレスポンスボディを解析する
Map<String,String> jsonMap = null;
StringBuilder json = new StringBuilder();
BufferedReader reader = new BufferedReader( new InputStreamReader( c.getInputStream() ) );
String line = null;
while ( (line = reader.readLine() ) != null)
{
json.append(line).append("\n");
}
reader.close();
c.disconnect();
if( c.getResponseCode() == 200 ){ jsonMap = cnvJSONToMap( json.toString() ); }
else{ System.out.println( "Response Code : " + c.getResponseCode() ); }
return jsonMap;
}
/**
* 簡易JSONパーサー
* @param strJSON
* @return
*/
protected Map<String,String> cnvJSONToMap( String strJSON )
{
// 入力チェック
if( strJSON == null || strJSON.equals("") ){ return null; }
// 戻り値を作成
HashMap<String,String> map = new HashMap<String,String>();
// 括弧を消す
String json = strJSON.replaceAll( "\\{", "" ).replaceAll( "\\}" , "" );
// 文字列を解析して、連想配列(マップ)に格納
String[] jsons = json.split( "," );
for( String line : jsons )
{
// すべてのダブルクオーテーションを消す
line = line.replaceAll( "\"", "" );
// Key値を取得
int keyStart = 0;
int keyEnd = line.indexOf( ":" , keyStart ) ;
String key = line.substring( keyStart , keyEnd ).trim();
// Value値を取得
int valueStart = keyEnd + 1;
int valueEnd = line.length();
String value = line.substring( valueStart , valueEnd ).trim();
// 連想配列に格納
map.put( key , value );
}
// 戻り値を返す
return map;
}
}
◇実行結果
print your userinfo.
name = ○○ ○○
id = ○○ ○○
given_name = ○○
locale = ja
family_name = ○○
picture = https://○○/○○.jpg
◇解説
OAuthを利用する部分は56行目~105行目である。最初にページへアクセスした場合はGETパラメータ「state」がnullになっているため、Google APIの認証サイトへリダイレクトする(58行目~68行目)。このとき、ユーザにはGoogle API認証画面が表示されるので承認する。
承認されるとGoogle APIはリダイレクト先URL「http://localhost:8888/OAuth2Test」をGETパラメータ「state=dummy」で呼び出す。このとき「authorization code」も返されるため、GAEプログラムではこれを元にgetTokenMap関数を呼び出し、Google APIへトークンを要求している(76行目)。getTokenMap関数内部(119行目~156行目)を見てみると、内部でHTTPリクエストを作成・発行し、戻ってきたレスポンス(JSON形式)からトークンを取得し連想配列に格納している。
その後、取得したトークンの中から「access token」を取得し、getJSONMap関数によりGoogle APIからユーザ情報を取得している(79行目)。そうして得られた情報をユーザに対して表示しているのが、82行目~99行目である。
■ 参照
- Google App Engine Documentations:OAuth API for Java
- Google Identity Platform 「 Using OAuth 2.0 to Access Google APIs」
- Google Developers 「OAuth 2.0 PlayGround」
- exception think 「Google Apps API Japan #3 GData API ハンズオン に参加してきた」