OAuth2.0 の アクセストークンを Google から取得する方法

このクックブックでは、OAuth2.0 の アクセストークンを Google から取得する方法を説明します。

intra-mart Accel Platformでは、OAuth2.0 の アクセストークンをユーザ毎に取得・保存する機能が備わっています。主な機能は以下のとおりです。

  • 初回のアクセストークン取得に必要な、認可要求に関する一連の画面や処理を共通化して提供
  • アクセストークンの有効期限が切れていた場合、リフレッシュトークンを利用して自動的にアクセストークンを更新

これにより、開発者は、OAuth2.0関連の面倒な実装をすることなく、外部のサービスが提供する様々な Web API へのアクセスを簡単に作成することが可能となります。

今回は、Google Drive™ のファイル一覧を取得することを例として、詳細な手順を説明します。

完成サンプル

以下の完成サンプルをダウンロードしてご活用ください。

e builder プロジェクト : im_cookbook_113639_e_builder_prj.zip
imm ファイル : im_cookbook_113639_oauth_client_google-1.0.0.imm

なお、ベースURLである以下の部分は、環境に合わせて適宜変更してください。
http://localhost:8080/imart

レシピ

概要

Google側の手順

  • 手順1. アプリケーション登録 - OAuth 2.0 クライアント ID の作成

intra-mart Accel Platform側の手順

  • 手順2. OAuthプロバイダ設定の作成。(/conf/oauth-provider-config/*.xmlの作成)
  • 手順3. 認可コード要求時のパラメータをGoogle用にカスタマイズ。(Google用のAuthzCodeRequestクラスの作成)
  • 手順4. アクセストークン更新時の成功レスポンスに refresh_token が含まれていない場合のカスタマイズ。(OAuthRefreshAccessTokenRequestProcessorの実装クラスを作成)
  • 手順5. カスタマイズしたクラスを利用するための OAuthRequestProcessorFactory を作成。
  • 手順6. 動作確認用サンプルの作成

Google側の手順

手順1. OAuth 2.0 クライアント ID の作成

最初に OAuth 2.0 クライアント ID を作成します。

OAuth 2.0を利用したGoogleのAPIアクセス方法に関する詳細は、Using OAuth 2.0 to Access Google APIs を参照してください。

1. Google Developers Consoleにアクセスします。

https://console.developers.google.com/

2. Google Drive API を検索します。


3. APIを有効にするためにプロジェクトを作成します。

4. Google Drive API を有効にします。


5. 認証情報を作成します。

[Go to Credentials]をクリックし、必要な認証情報の種類を指定します。

API を呼び出す場所(=Where will you be calling the API from?)は、
「ウェブサーバ(=Web Server)」を選択します。

アクセスするデータの種類(=What data will you be accessing?) は、
「ユーザデータ(=User data)」を選択します。

6. 必要な認証情報を入力します。

承認済みのリダイレクト URI(=Authorized redirect URIs) に「http://example.org/imart/oauth/redirect」を設定します。
http://example.org/imart 部分は、intra-mart Accel Platform が稼働しているURLを指定してください。

今回は、ローカルマシン上にintra-mart Accel Platform 環境を構築することを前提として、以下を入力してください。

http://localhost:8080/imart/oauth/redirect

7. ユーザに表示するサービス名 を設定します。

今回は、「intra-mart Accel Platform Linkage Sample for Google」と入力します。


8. クライアントID と クライアント シークレット を取得します。

左側のメニュー「認証情報(=Credentials)」より、「intra-mart Accel Platform Linkage Sample」を選択します。

ここに表示された「クライアントID(=Client ID)」と「クライアント シークレット(=Client secret)」を覚えておいてください。
後述の OAuthプロバイダ設定の作成で利用します。


intra-mart Accel Platform側の手順

手順2. OAuthプロバイダ設定の作成。(/conf/oauth-provider-config/*.xmlの作成)

e Builder のモジュールプロジェクト内に「OAuthプロバイダ設定ファイル」を作成します。
OAuthプロバイダ設定ファイルには、外部連携アプリケーションで利用するOAuthプロバイダの情報を設定します。
詳しくは、 設定ファイルリファレンスを参照してください。

src/main/conf/oauth-provider-config/im_cookbook_113639_oauth_client_google.xml
<?xml version="1.0" encoding="UTF-8"?>
<oauth-provider-config xmlns="http://www.intra-mart.jp/system/oauth/client/config/oauth-provider-config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/system/oauth/client/config/oauth-provider-config ../schema/oauth-provider-config.xsd ">

  <oauth-providers>

    <oauth-provider id="im_cookbook_113639_oauth_client_google">
      <provider-type>google</provider-type>

      <name message-cd="CAP.Z.IWP.GOOGLE.OAUTH.PROVIDER.NAME">Google Drive™ API - im_cookbook_113639</name>
      <description message-cd="CAP.Z.IWP.GOOGLE.OAUTH.PROVIDER.DESCRIPTION">It is allow the use of the Google's Drive API.</description>
      <icon-path>im_cookbook_113639_oauth_client_google/images/google-drive-icon_48.png</icon-path>
      <oauth-config>
        <authz-end-point>https://accounts.google.com/o/oauth2/auth</authz-end-point>
        <token-end-point>https://accounts.google.com/o/oauth2/token</token-end-point>

        <client-id>[Please input your application's Client ID]</client-id>
        <client-secret>[Please input your application's Client Secret]</client-secret>
        <scope>https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/drive.photos.readonly</scope>

      </oauth-config>
      <extra-config>
      </extra-config>
    </oauth-provider>

  </oauth-providers>

</oauth-provider-config>

行数|説明|
:--|:--|
18|先ほど取得した「クライアントID」を設定します。|
19|先ほど取得した「クライアント シークレット」を設定します。|
20|スコープを設定します。今回は、Google Drive 上のファイルを参照する為のスコープを設定します。
なお、Googleが公開しているスコープ一覧は、以下を参照してください。
OAuth 2.0 Scopes for Google APIs|

手順3. 認可コード要求時のパラメータをGoogle用にカスタマイズ。(Google用のAuthzCodeRequestクラスの作成)

iAPでは、OAuth2.0の認可コードグラントに対応しています。
OAuth 2.0 における 認可リクエスト のパラメータは以下の通りです。
(詳細は 4.1.1. Authorization Request を参照してください)

  • response_type
  • client_id
  • redirect_uri
  • scope
  • state

認可サーバをGoogleとした場合、上記に加えて以下のパラメータが必要です。

パラメータ名 説明
access_type refresh_token を取得するために「offline」と設定します。
approval_prompt 認証を強制するために「force」と設定します。

パラメータの詳細は、以下を参照してください。

そこで、この2つのパラメータを追加するためのクラスを作成します。

src/main/java/jp/co/intra_mart/cookbook/im_cookbook_113639/oauth/client/service/processor/impl/GoogleAuthzCodeRequest.java
public class GoogleAuthzCodeRequest extends AuthzCodeRequest {

    public GoogleAuthzCodeRequest(final String providerId, final OAuthConnectionInfo connectionInfo) {
        super(providerId, connectionInfo);

        addParameter(GoogleConstants.PARAM_NAME_4_ACCESS_TYPE, "offline");
        addParameter(GoogleConstants.PARAM_NAME_4_APPROVAL_PROMPT, "force");
    }

}
src/main/java/jp/co/intra_mart/cookbook/im_cookbook_113639/oauth/client/service/processor/impl/GoogleConstants.java
public final class GoogleConstants {

    public static final String PROVIDER_TYPE = "google"; //$NON-NLS-1$

    public static final String PARAM_NAME_4_ACCESS_TYPE = "access_type"; //$NON-NLS-1$

    public static final String PARAM_NAME_4_APPROVAL_PROMPT = "approval_prompt"; //$NON-NLS-1$

    private GoogleConstants() {
    }

}

手順4. アクセストークン更新時の成功レスポンスに refresh_token が含まれていない場合のカスタマイズ。(OAuthRefreshAccessTokenRequestProcessorの実装クラスを作成)

OAuth 2.0 における 認可リクエスト のパラメータは以下の通りです。
(詳細は 5.1. Successful Response を参照してください)

  • access_token
  • token_type
  • expires_in
  • refresh_token
  • scope

Googleでは、アクセストークン更新時の成功レスポンスに refresh_token が含まれていません。
そこで、以前のリフレッシュトークンを再利用するためのクラスを作成します。

src/main/java/jp/co/intra_mart/cookbook/im_cookbook_113639/oauth/client/service/processor/impl/RefreshAccessTokenRequestProcessorWithoutRefreshTokenOnSuccessResponse.java
public class RefreshAccessTokenRequestProcessorWithoutRefreshTokenOnSuccessResponse extends AbstractRefreshAccessTokenRequestProcessor {

    private final ThreadLocal previousRefreshTokens = new ThreadLocal();

    protected RefreshAccessTokenRequestProcessorWithoutRefreshTokenOnSuccessResponse(final String providerType) {
        super(providerType);
    }

    @Override
    protected RefreshTokenRequest createRequest(final String providerId, final String refreshToken) throws OAuthClientException {
        previousRefreshTokens.set(refreshToken);
        return super.createRequest(providerId, refreshToken);
    }

    @Override
    protected AccessTokenInfo parseResponse(final String providerId, final ResponseModel<HttpEntityModel> response) throws OAuthClientException, OAuthRequestException {
        try {
            final AccessTokenInfo accessTokenInfo = super.parseResponse(providerId, response);
            final String refreshTokenOnResponse = accessTokenInfo.getRefreshToken();

            // アクセストークン更新時の成功レスポンスに refresh_token が含まれていない場合は、以前のリフレッシュトークンを再利用する。
            if (refreshTokenOnResponse == null || refreshTokenOnResponse.isEmpty()) {
                accessTokenInfo.setRefreshToken(previousRefreshTokens.get());
            }

            return accessTokenInfo;
        } finally {
            previousRefreshTokens.remove();
        }
    }

    @Override
    protected void validateToken(final AccessTokenInfo tokenInfo) throws OAuthResponseParseException {
        try {
            super.validateToken(tokenInfo);
        } catch (final OAuthResponseParseException e) {
            // アクセストークン更新時の成功レスポンスに refresh_token が含まれていない場合、エラーとしない。
            if (e.getMessage().contains(OAuthClientLog.E_IWP_OAUTHCLIENT_PROCESSOR_00016.getWithCode())) {
                return;
            } else {
                throw e;
            }
        }
    }

}

手順5. カスタマイズしたクラスを利用するための OAuthRequestProcessorFactory を作成。

src/main/resources/META-INF/services/jp.co.intra_mart.system.oauth.client.service.processor.OAuthRequestProcessorFactory
jp.co.intra_mart.cookbook.im_cookbook_113639.oauth.client.service.processor.impl.GoogleOAuthRequestProcessorFactory
src/main/java/jp/co/intra_mart/cookbook/im_cookbook_113639/oauth/client/service/processor/impl/GoogleOAuthRequestProcessorFactory.java
public class GoogleOAuthRequestProcessorFactory extends StandardOAuthRequestProcessorFactory {

    private final OAuthAuthzCodeRequestProcessor authzCodeProcessor;

    private final OAuthRefreshAccessTokenRequestProcessor refreshTokneProcessor;

    public GoogleOAuthRequestProcessorFactory() {
        authzCodeProcessor = new GoogleAuthzCodeRequestProcessor();

        // Googleの場合、アクセストークン更新時の成功レスポンスには refresh_token が含まれない(2016年4月現在)
        refreshTokneProcessor = new RefreshAccessTokenRequestProcessorWithoutRefreshTokenOnSuccessResponse(GoogleConstants.PROVIDER_TYPE);
    }

    @Override
    public OAuthAuthzCodeRequestProcessor getAuthzCodeRequestProcessor() {
        return authzCodeProcessor;
    }

    @Override
    public String getProviderType() {
        return GoogleConstants.PROVIDER_TYPE;
    }

    @Override
    public OAuthRefreshAccessTokenRequestProcessor getRefreshAccessTokenRequestProcessor() {
        return refreshTokneProcessor;
    }

}
src/main/java/jp/co/intra_mart/cookbook/im_cookbook_113639/oauth/client/service/processor/impl/GoogleAuthzCodeRequestProcessor.java
public class GoogleAuthzCodeRequestProcessor extends AbstractAuthzCodeRequestProcessor {

    public GoogleAuthzCodeRequestProcessor() {
        super(GoogleConstants.PROVIDER_TYPE);
    }

    @Override
    protected AuthzCodeRequest createRequestInstance(final String providerId) throws OAuthClientException {
        final OAuthConnectionInfo connectionInfo = OAuthProviderConfigurationFactory.getInstance().getConnectionInfoAccessor().getConnectionInfo(providerId);
        return new GoogleAuthzCodeRequest(providerId, connectionInfo);
    }

}

手順6. 動作確認用サンプルの作成

Google Drive 上のファイル一覧を表示するJSPを作成します。

src/main/webapp/im_cookbook_113639_oauth_client_google/google_drive_file_lists.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

<%@ page import="java.util.LinkedHashMap"%>
<%@ page import="java.util.Map"%>

<%@ page import="jp.co.intra_mart.cookbook.im_cookbook_113639.oauth.client.service.processor.impl.GoogleConstants"%>

<%@ page import="jp.co.intra_mart.foundation.context.Contexts"%>
<%@ page import="jp.co.intra_mart.foundation.context.model.AccountContext"%>
<%@ page import="jp.co.intra_mart.foundation.oauth.client.model.AccessTokenInfo"%>
<%@ page import="jp.co.intra_mart.foundation.oauth.client.service.OAuthClientService"%>
<%@ page import="jp.co.intra_mart.foundation.oauth.client.service.OAuthClientServiceFactory"%>

<%@ page import="org.apache.commons.httpclient.Header"%>
<%@ page import="org.apache.commons.httpclient.HttpClient"%>
<%@ page import="org.apache.commons.httpclient.methods.GetMethod"%>

<%
final String uri = "https://www.googleapis.com/drive/v3/files";

final String userCd = Contexts.get(AccountContext.class).getUserCd();
final String providerId = "im_cookbook_113639_oauth_client_google";

final OAuthClientService oAuthClient = OAuthClientServiceFactory.getInstance().getOAuthClientService(GoogleConstants.PROVIDER_TYPE);
final AccessTokenInfo token = oAuthClient.getAccessToken(userCd, providerId);

int status;
Map responseHeaders;
String responseBodyAsString;

final GetMethod httpRequest = new GetMethod(uri);
try{
    httpRequest.addRequestHeader("Authorization", token.getTokenType() + " " + token.getAccessToken()); // <-- Set AccessToken
    httpRequest.addRequestHeader("Content-Type", "application/json; charset=UTF-8");
    httpRequest.addRequestHeader("Accept", "application/json");

    status = new HttpClient().executeMethod(httpRequest);

    responseHeaders = new LinkedHashMap();
    for (final Header header : httpRequest.getResponseHeaders()) {
        responseHeaders.put(header.getName(), header.getValue());
    }

    responseBodyAsString = httpRequest.getResponseBodyAsString();
} finally {
    httpRequest.releaseConnection();
}

%>

<table border="1">
    <tr>
        <th>status</th>
        <td><%=status%></td>
    </tr>
    <tr>
        <th>responseHeaders</th>
        <td><pre><%=responseHeaders%></pre></td>
    </tr>
    <tr>
        <th>responseBodyAsString</th>
        <td><pre><%=responseBodyAsString%></pre></td>
    </tr>
</table>
行数 説明
19 Google Drive の ファイル一覧を取得為のURIを指定します。
Google Drive APIの詳細は、こちら を参照してください。
24〜25 OAuthClientServiceを使う事で、アクセストークンが簡単に取得可能となります。

動作確認

1. intra-mart Accel Platformにログインします。

ここでは、サンプルユーザ「ueda」でログインします。

2. 右上のユーティリティメニューより、[個人設定] - [外部連携アプリケーション]を選択します。

3. 「Google Drive API - im_cookbook_113639」を [許可] します。

4. Google アカウントでログインします。

5. サービスを許可します。


6. サンプル画面「google_drive_file_lists.jsp」にアクセスします。

http://localhost:8080/imart/im_cookbook_113639_oauth_client_google/google_drive_file_lists.jsp にアクセスします。

このGoogleユーザは、Google Drive上のファイルがひとつしか存在しないようです。
試しに、Google Driveにファイルを2つアップロードしてみます。

再度、http://localhost:8080/imart/im_cookbook_113639_oauth_client_google/google_drive_file_lists.jsp にアクセスします。
ファイルの一覧が取得できました。

まとめ

この CookBook では、OAuth2.0 の アクセストークンを Google から取得する方法を紹介しました。

Google以外でも、OAuth 2.0対応の認可サーバであれば、この CookBook と同様の方法でアクセストークンを取得することが可能です。
お客様の用途にあわせて是非ご活用ください。