OAuth 2.0 と Java 用 Google OAuth クライアント ライブラリ

概要

目的: このドキュメントでは、Java 用 Google OAuth クライアント ライブラリで提供される一般的な OAuth 2.0 関数について説明します。これらの関数は、任意のインターネット サービスの認証と認可に使用できます。

GoogleCredential を使用して Google サービスで OAuth 2.0 認可を行う手順については、Java 用 Google API クライアント ライブラリでの OAuth 2.0 の使用をご覧ください。

概要: OAuth 2.0 は、エンドユーザーが保護されたサーバーサイド リソースにアクセスするクライアント アプリケーションを安全に承認できるようにするための標準仕様です。また、OAuth 2.0 ベアラートークンの仕様では、エンドユーザーの認可プロセスで付与されたアクセス トークンを使用して、保護されたリソースにアクセスする方法について説明しています。

詳細については、次のパッケージの Javadoc ドキュメントをご覧ください。

クライアントの登録

Java 用 Google OAuth クライアント ライブラリを使用する前に、認可サーバーにアプリケーションを登録してクライアント ID とクライアント シークレットを取得する必要があります。(このプロセスの一般的な情報については、クライアント登録の仕様をご覧ください)。

認証情報と認証情報ストア

認証情報は、アクセス トークンを使用して保護されたリソースにアクセスするためのスレッドセーフの OAuth 2.0 ヘルパークラスです。更新トークンを使用する場合、Credential は更新トークンを使用してアクセス トークンが期限切れになると、アクセス トークンも更新します。たとえば、すでにアクセス トークンがある場合は、次の方法でリクエストできます。

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

ほとんどのアプリでは、ブラウザの認可ページにリダイレクトされないように、認証情報のアクセス トークンと更新トークンを保持する必要があります。このライブラリの CredentialStore 実装は非推奨であり、今後のリリースで削除される予定です。別の方法として、Java 用 Google HTTP クライアント ライブラリで提供される StoredCredentialDataStoreFactory インターフェースと DataStore インターフェースを使用することもできます。

ライブラリで提供されている次のいずれかの実装を使用できます。

  • JdoDataStoreFactory は、JDO を使用して認証情報を保持します。
  • AppEngineDataStoreFactory は、Google App Engine Data Store API を使用して認証情報を保持します。
  • MemoryDataStoreFactory は、認証情報をメモリに「保持」します。これは、プロセスの存続期間の短期ストレージとしてのみ有用です。
  • FileDataStoreFactory は、認証情報をファイルに保持します。

Google App Engine ユーザー:

AppEngineCredentialStore は非推奨となり、削除される予定です。

StoredCredentialAppEngineDataStoreFactory を使用することをおすすめします。認証情報が古い方法で保存されている場合は、追加されたヘルパー メソッド migrateTo(AppEngineDataStoreFactory) または migrateTo(DataStore) を使用して移行できます。

DataStoreCredentialRefreshListener を使用して、GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener) を使用して認証情報に設定します。

認証コードのフロー

認可コードフローを使用して、エンドユーザーが保護されたデータへのアクセスをアプリに許可できるようにします。このフローのプロトコルは、Authorization Code Grant の仕様で指定されています。

このフローは、AuthorizationCodeFlow を使用して実装されています。ステップは次のとおりです。

  • エンドユーザーがアプリケーションにログインします。そのユーザーを、アプリに固有のユーザー ID に関連付ける必要があります。
  • ユーザー ID に基づいて AuthorizationCodeFlow.loadCredential(String) を呼び出し、ユーザーの認証情報が既知であるかどうかを確認します。動作すれば完了です。
  • そうでない場合は、AuthorizationCodeFlow.newAuthorizationUrl() を呼び出して、エンドユーザーのブラウザを認可ページにリダイレクトします。このページで、エンドユーザーは保護されたデータへのアクセスをアプリに許可できます。
  • ウェブブラウザは、コード クエリ パラメータを含むリダイレクト URL にリダイレクトします。このパラメータは、AuthorizationCodeFlow.newTokenRequest(String) を使用してアクセス トークンをリクエストするために使用できます。
  • AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String) を使用して、保護されたリソースにアクセスするための認証情報を保存して取得します。

AuthorizationCodeFlow を使用していない場合は、下位レベルのクラスを使用することもできます。

サーブレットの認可コードフロー

このライブラリには、基本的なユースケースの認可コードフローを大幅に簡素化するサーブレット ヘルパー クラスが用意されています。AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServletgoogle-oauth-client-servlet から)の具体的なサブクラスを指定して、web.xml ファイルに追加するだけです。その場合も、ウェブ アプリケーションへのユーザー ログインの処理とユーザー ID の抽出は必要です。

サンプルコード:

public class ServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class ServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

Google App Engine の認可コードフロー

App Engine の認可コード フローは、Google App Engine の Users Java API を利用できることを除き、サーブレットの認可コード フローとほぼ同じです。Users Java API を有効にするには、ユーザーがログインしている必要があります。ユーザーがまだログインしていない場合にログインページにリダイレクトする方法については、セキュリティと認証(web.xml 内)をご覧ください。

サーブレットの場合との主な違いは、AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServletgoogle-oauth-client-appengine から)の具体的なサブクラスを指定することです。抽象サーブレット クラスを拡張し、Users Java API を使用して getUserId メソッドを実装します。AppEngineDataStoreFactoryJava 用 Google HTTP クライアント ライブラリ)は、Google App Engine Data Store API を使用して認証情報を保持する場合に適しています。

サンプルコード:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

コマンドライン認証コードフロー

dailymotion-cmdline-sample から取得した簡素化されたサンプルコード:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

ブラウザベースのクライアントのフロー

暗黙的付与の仕様で指定されているブラウザベースのクライアントフローの一般的な手順は次のとおりです。

  • BrowserClientRequestUrl を使用して、エンドユーザーのブラウザを認証ページにリダイレクトします。このページで、エンドユーザーは保護されたデータへのアクセスをアプリケーションに許可できます。
  • JavaScript アプリケーションを使用して、認可サーバーに登録されているリダイレクト URI の URL フラグメントにあるアクセス トークンを処理します。

ウェブ アプリケーションでの使用例:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

期限切れのアクセス トークンの検出

OAuth 2.0 ベアラの仕様に従い、期限切れのアクセス トークンを使用して保護されたリソースにアクセスするためにサーバーが呼び出されると、サーバーは通常、次のような HTTP 401 Unauthorized ステータス コードで応答します。

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

ただし、仕様には多くの柔軟性があるようです。詳細については、OAuth 2.0 プロバイダのドキュメントをご覧ください。

別の方法として、アクセス トークンのレスポンスexpires_in パラメータを確認する方法もあります。これは、許可されたアクセス トークンの存続時間を秒単位で指定します。通常は 1 時間です。ただし、実際にはアクセス トークンがその期間の終了時に期限切れにならないため、サーバーはアクセスを許可し続ける場合があります。そのため、通常は、経過時間に基づいてトークンが期限切れになったと想定するのではなく、401 Unauthorized ステータス コードを待つことをおすすめします。または、有効期限が切れる直前にアクセス トークンを更新することもできます。トークン サーバーが使用できない場合は、401 を受信するまでアクセス トークンを使い続けます。これは、認証情報でデフォルトで使用される戦略です。

別の方法として、リクエストのたびに新しいアクセス トークンを取得することもできますが、その場合、トークン サーバーに毎回追加の HTTP リクエストを送信する必要があるため、速度とネットワーク使用量の点で適切ではありません。理想的には、アクセス トークンを安全で永続的なストレージに保存して、アプリケーションによる新しいアクセス トークンのリクエストを最小限に抑えます。(ただし、インストール済みのアプリの場合、安全なストレージは難しい問題です)。

アクセス トークンは、期限切れ以外の理由(ユーザーがトークンを明示的に取り消した場合など)で無効になる可能性があるため、エラー処理コードが堅牢であることを確認してください。トークンが有効ではないことを検出した場合(期限切れや取り消しなど)、ストレージからアクセス トークンを削除する必要があります。たとえば Android では、AccountManager.invalidateAuthToken を呼び出す必要があります。