ビルド承認コールバック ハンドラ

このドキュメントでは、Google Tasks API を使用してユーザーのタスクを表示するサンプル ウェブ アプリケーションから、Java サーブレットを使用して OAuth 2.0 認証コールバック ハンドラを実装する方法について説明します。サンプル アプリケーションは、まずユーザーの Google ToDo リストへのアクセス承認をリクエストし、デフォルトのタスクリストにユーザーのタスクを表示します。

オーディエンス

このドキュメントは、Java と J2EE のウェブ アプリケーション アーキテクチャに精通しているユーザーを対象としています。OAuth 2.0 承認フローについてある程度の知識があることが推奨されます。

目次

サンプルを完全に機能させるには、いくつかのステップが必要です。

web.xml ファイルでサーブレット マッピングを宣言する

アプリケーションでは 2 つのサーブレットを使用します。

  • PrintTasksTitlesServlet/ にマッピング): ユーザー認証を処理し、ユーザーのタスクを表示するアプリケーションのエントリ ポイント。
  • OAuthCodeCallbackHandlerServlet/oauth2callback にマッピング): OAuth 認可エンドポイントからのレスポンスを処理する OAuth 2.0 コールバック

以下の web.xml ファイルでは、これら 2 つのサーブレットをアプリケーションの URL にマッピングしています。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
   
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

 
<servlet>
   
<servlet-name>PrintTasksTitles</servlet-name>
   
<servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class>
 
</servlet>

 
<servlet-mapping>
   
<servlet-name>PrintTasksTitles</servlet-name>
   
<url-pattern>/</url-pattern>
 
</servlet-mapping>

 
<servlet>
   
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   
<servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class>
 
</servlet>

 
<servlet-mapping>
   
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   
<url-pattern>/oauth2callback</url-pattern>
 
</servlet-mapping>

</web-app>
/WEB-INF/web.xml ファイル

システム上でユーザーを認証し、そのタスクにアクセスするための承認をリクエストする

ユーザーがルート「/」PrintTaskListsTitlesServlet サーブレットにマッピングされる URL。このサーブレットでは、次のタスクが実行されます。

  • ユーザーがシステムで認証されているかどうかを確認する
  • ユーザーが認証されていない場合は、認証ページにリダイレクトされます。
  • ユーザーが認証された場合は、更新トークンがデータ ストレージにすでにあるかどうかを確認します。更新トークンは以下の OAuthTokenDao で処理されます。ユーザーが利用できる更新トークンがない場合は、ユーザーがまだアプリケーションにタスクへのアクセスを許可していないことを意味します。その場合、ユーザーは Google の OAuth 2.0 認可エンドポイントにリダイレクトされます。
で確認できます。 これを実装する方法は次のとおりです。

package com.google.oauthsample;

import ...

/**
 * Simple sample Servlet which will display the tasks in the default task list of the user.
 */

@SuppressWarnings("serial")
public class PrintTasksTitlesServlet extends HttpServlet {

 
/**
   * The OAuth Token DAO implementation, used to persist the OAuth refresh token.
   * Consider injecting it instead of using a static initialization. Also we are
   * using a simple memory implementation as a mock. Change the implementation to
   * using your database system.
   */

 
public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();

 
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
   
// Getting the current user
   
// This is using App Engine's User Service but you should replace this to
   
// your own user/login implementation
   
UserService userService = UserServiceFactory.getUserService();
   
User user = userService.getCurrentUser();

   
// If the user is not logged-in it is redirected to the login service, then back to this page
   
if (user == null) {
      resp
.sendRedirect(userService.createLoginURL(getFullRequestUrl(req)));
     
return;
   
}

   
// Checking if we already have tokens for this user in store
   
AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail());

   
// If we don't have tokens for this user
   
if (accessTokenResponse == null) {
     
OAuthProperties oauthProperties = new OAuthProperties();
     
// Redirect to the Google OAuth 2.0 authorization endpoint
      resp
.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
         
OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
             
.getScopesAsString()).build());
     
return;
   
}
 
}

 
/**
   * Construct the request's URL without the parameter part.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */

 
public static String getFullRequestUrl(HttpServletRequest req) {
   
String scheme = req.getScheme() + "://";
   
String serverName = req.getServerName();
   
String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
   
String contextPath = req.getContextPath();
   
String servletPath = req.getServletPath();
   
String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
   
String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString();
   
return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString;
 
}
}
PrintTasksTitlesServlet.java ファイル

注: 上記の実装では一部の App Engine ライブラリを使用していますが、簡略化のために使用されています。別のプラットフォーム向けに開発している場合は、ユーザー認証を処理する UserService インターフェースを再実装できます。

アプリケーションは DAO を使用してユーザーの認証トークンを保持し、アクセスします。以下は、このサンプルで使用されているインターフェース OAuthTokenDao とモック(メモリ内)実装 OAuthTokenDaoMemoryImpl です。

package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;

/**
 * Allows easy storage and access of authorization tokens.
 */

public interface OAuthTokenDao {

 
/**
   * Stores the given AccessTokenResponse using the {@code username}, the OAuth
   * {@code clientID} and the tokens scopes as keys.
   *
   * @param tokens The AccessTokenResponse to store
   * @param userName The userName associated wit the token
   */

 
public void saveKeys(AccessTokenResponse tokens, String userName);

 
/**
   * Returns the AccessTokenResponse stored for the given username, clientId and
   * scopes. Returns {@code null} if there is no AccessTokenResponse for this
   * user and scopes.
   *
   * @param userName The username of which to get the stored AccessTokenResponse
   * @return The AccessTokenResponse of the given username
   */

 
public AccessTokenResponse getKeys(String userName);
}
OAuthTokenDao.java ファイル
package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;
...

/**
 * Quick and Dirty memory implementation of {@link OAuthTokenDao} based on
 * HashMaps.
 */

public class OAuthTokenDaoMemoryImpl implements OAuthTokenDao {

 
/** Object where all the Tokens will be stored */
 
private static Map
OAuthTokenDaoMemoryImpl.java ファイル

また、アプリケーションの OAuth 2.0 認証情報はプロパティ ファイルに格納されています。または、Java クラスのどこかで定数として指定することもできますが、サンプルで使用されている OAuthProperties クラスと oauth.properties ファイルを以下に示します。

package com.google.oauthsample;

import ...

/**
 * Object representation of an OAuth properties file.
 */

public class OAuthProperties {

 
public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties";

 
/** The OAuth 2.0 Client ID */
 
private String clientId;

 
/** The OAuth 2.0 Client Secret */
 
private String clientSecret;

 
/** The Google APIs scopes to access */
 
private String scopes;

 
/**
   * Instantiates a new OauthProperties object reading its values from the
   * {@code OAUTH_PROPERTIES_FILE_NAME} properties file.
   *
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OauthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */

 
public OAuthProperties() throws IOException {
   
this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME));
 
}

 
/**
   * Instantiates a new OauthProperties object reading its values from the given
   * properties file.
   *
   * @param propertiesFile the InputStream to read an OAuth Properties file. The
   *          file should contain the keys {@code clientId}, {@code
   *          clientSecret} and {@code scopes}
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OAuthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */

 
public OAuthProperties(InputStream propertiesFile) throws IOException {
   
Properties oauthProperties = new Properties();
    oauthProperties
.load(propertiesFile);
    clientId
= oauthProperties.getProperty("clientId");
    clientSecret
= oauthProperties.getProperty("clientSecret");
    scopes
= oauthProperties.getProperty("scopes");
   
if ((clientId == null) || (clientSecret == null) || (scopes == null)) {
     
throw new OAuthPropertiesFormatException();
   
}
 
}

 
/**
   * @return the clientId
   */

 
public String getClientId() {
   
return clientId;
 
}

 
/**
   * @return the clientSecret
   */

 
public String getClientSecret() {
   
return clientSecret;
 
}

 
/**
   * @return the scopes
   */

 
public String getScopesAsString() {
   
return scopes;
 
}

 
/**
   * Thrown when the OAuth properties file was not at the right format, i.e not
   * having the right properties names.
   */

 
@SuppressWarnings("serial")
 
public class OAuthPropertiesFormatException extends RuntimeException {
 
}
}
OAuthProperties.java ファイル

以下の oauth.properties ファイルです。このファイルには、アプリケーションの OAuth 2.0 認証情報が含まれています。以下の値はご自身で変更する必要があります。

# Client ID and secret. They can be found in the APIs console.
clientId
=1234567890.apps.googleusercontent.com
clientSecret
=aBcDeFgHiJkLmNoPqRsTuVwXyZ
# API scopes. Space separated.
scopes
=https://www.googleapis.com/auth/tasks
oauth.properties ファイル

OAuth 2.0 クライアント ID とクライアント シークレットによってアプリケーションが識別され、アプリケーション用に定義されたフィルタと割り当てルールを Tasks API が適用できるようになります。クライアント ID とシークレットは Google API コンソールで確認できます。コンソールで以下を行う必要があります。

  • プロジェクトを作成または選択します。
  • サービスのリストで Tasks API のステータスを [オン] に切り替えて、Tasks API を有効にします。
  • [API Access] で OAuth 2.0 クライアント ID を作成します(まだ作成していない場合)。
  • プロジェクトの OAuth 2.0 コード コールバック ハンドラの URL がリダイレクト URI で登録またはホワイトリストに登録されていることを確認します。たとえば、このサンプル プロジェクトでは、ウェブ アプリケーションが https://www.example.com ドメインから提供される場合、https://www.example.com/oauth2callback を登録する必要があります。

API コンソールのリダイレクト URI
API コンソールのリダイレクト URI

Google の認可エンドポイントからの認可コードをリッスンする

ユーザーがまだアプリケーションによるタスクへのアクセスを承認しておらず、そのため Google の OAuth 2.0 認可エンドポイントにリダイレクトされている場合、アプリケーションにタスクへのアクセスを許可するよう求める Google からの承認ダイアログがユーザーに表示されます。

Google の承認ダイアログ
Google の承認ダイアログ

アクセスを許可または拒否すると、ユーザーは OAuth 2.0 コードのコールバック ハンドラにリダイレクトされます。このハンドラは、Google 認証 URL の作成時にリダイレクト/コールバックとして指定されています。

new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
     
OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
         
.getScopesAsString()).build()

OAuth 2.0 コードのコールバック ハンドラ(OAuthCodeCallbackHandlerServlet)は、Google OAuth 2.0 エンドポイントからのリダイレクトを処理します。次の 2 つのケースを処理します。

  • ユーザーがアクセスを許可した場合: リクエストを解析して URL パラメータから OAuth 2.0 コードを取得します
  • ユーザーがアクセスを拒否した: ユーザーにメッセージが表示されます。

package com.google.oauthsample;

import ...

/**
 * Servlet handling the OAuth callback from the authentication service. We are
 * retrieving the OAuth code, then exchanging it for a refresh and an access
 * token and saving it.
 */

@SuppressWarnings("serial")
public class OAuthCodeCallbackHandlerServlet extends HttpServlet {

 
/** The name of the Oauth code URL parameter */
 
public static final String CODE_URL_PARAM_NAME = "code";

 
/** The name of the OAuth error URL parameter */
 
public static final String ERROR_URL_PARAM_NAME = "error";

 
/** The URL suffix of the servlet */
 
public static final String URL_MAPPING = "/oauth2callback";

 
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
   
// Getting the "error" URL parameter
   
String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME);

   
// Checking if there was an error such as the user denied access
   
if (error != null && error.length > 0) {
      resp
.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\".");
     
return;
   
}
   
// Getting the "code" URL parameter
   
String[] code = req.getParameterValues(CODE_URL_PARAM_NAME);

   
// Checking conditions on the "code" URL parameter
   
if (code == null || code.length == 0) {
      resp
.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing");
     
return;
   
}
 
}

 
/**
   * Construct the OAuth code callback handler URL.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */

 
public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) {
   
String scheme = req.getScheme() + "://";
   
String serverName = req.getServerName();
   
String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
   
String contextPath = req.getContextPath();
   
String servletPath = URL_MAPPING;
   
String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
   
return scheme + serverName + serverPort + contextPath + servletPath + pathInfo;
 
}
}
OAuthCodeCallbackHandlerServlet.java ファイル

認証コードを更新トークンとアクセス トークンに交換する

次に、OAuthCodeCallbackHandlerServlet は、Auth 2.0 コードをリフレッシュ トークンとアクセス トークンと交換し、それをデータストアに保持して、ユーザーを PrintTaskListsTitlesServlet URL にリダイレクトします。

以下のファイルに追加されたコードは構文がハイライト表示され、既存のコードはグレー表示になっています。


 
/** コールバック処理後にユーザーをリダイレクトする URL。検討事項
   * ユーザーを Google にリダイレクトする前に Cookie に保存する
   * ユーザーのリダイレクト先となる URL が複数ある場合は承認 URL。*/

 
public static final String REDIRECT_URL = "/";

 
/** OAuth トークン DAO 実装。代わりに挿入することを検討する
   * 静的初期化です。また、シンプルなメモリ実装を使用して、
   * 使用できます。データベース システムを使用するように実装を変更します。*/

 
public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();


   
// 受信リクエストの URL を作成する
   
String requestUrl = getOAuthCodeCallbackHandlerUrl(req);

   
// OAuth トークンのコードを交換
   
AccessTokenResponse accessTokenResponse = ExchangeCodeForAccessAndRefreshTokens(code[0],
        requestUrl
);

   
// 現在のユーザーを取得する
   
// これは App Engine のユーザー サービスを使用していますが、これを
   
// 独自のユーザー/ログイン実装
   
UserService userService = UserServiceFactory.getUserService();
   
String email = userService.getCurrentUser().getEmail();

   
// トークンを保存する
    oauthTokenDao
.saveKeys(accessTokenResponse, email);

    resp
.sendRedirect(REDIRECT_URL);
 
}


 
/**
   * 指定されたコードをエクスチェンジと更新トークンと交換します。
   *
   * @param code 認可サービスから返されたコード
   * @param currentUrl コールバックの URL
   * @param oauthProperties OAuth 構成を含むオブジェクト
   * @return アクセス トークンと更新トークンの両方を含むオブジェクト
   * @IOException をスロー
   */

 
public AccessTokenResponse ExchangeCodeForAccessAndRefreshTokens(String code, String currentUrl)
     
throws IOException {

   
HttpTransport httpTransport = new NetHttpTransport();
   
JacksonFactory jsonFactory = new JacksonFactory();

   
// OAuth 構成ファイルの読み込み
   
OAuthProperties oauthProperties = new OAuthProperties();

   
新しい GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties) が返される
       
.getClientId()、oauthProperties.getClientSecret()、コード、currentUrl).execute();
 
}
}

OAuthCodeCallbackHandlerServlet.java ファイル


注: 上記の実装では一部の App Engine ライブラリを使用していますが、簡略化のために使用されています。別のプラットフォーム向けに開発している場合は、ユーザー認証を処理する UserService インターフェースを再実装できます。



ユーザーのタスクを読み取って表示する



ユーザーがアプリケーションにタスクへのアクセスを許可しました。アプリケーションの更新トークンはデータストアに保存され、OAuthTokenDao を使用してアクセスできます。これで、PrintTaskListsTitlesServlet サーブレットがこれらのトークンを使用してユーザーのタスクにアクセスし、タスクを表示できるようになりました。



以下のファイルに追加されたコードは構文がハイライト表示され、既存のコードはグレー表示になっています。