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 とクライアント シークレットを受け取るために、アプリケーションを認可サーバーに登録する必要があります。(このプロセスの一般的な情報については、クライアント登録の仕様をご覧ください)。

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

Credential は、アクセス トークンを使用して保護されたリソースにアクセスするためのスレッドセーフな 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 は、WebView を使用して認証情報を保持します。
  • AppEngineDataStoreFactory は Google App Engine Data Store API を使用して認証情報を保持します。
  • MemoryDataStoreFactory は認証情報をメモリに「保持」します。これは、プロセスの存続期間中の短期的なストレージとしてのみ有用です。
  • FileDataStoreFactory は認証情報をファイル内に保持します。

Google App Engine ユーザー:

AppEngineCredentialStore は非推奨になり、今後削除されます。

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

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

認可コードフロー

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

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

  • エンドユーザーがアプリケーションにログインします。そのユーザーをアプリケーションに固有のユーザー ID に関連付ける必要があります。
  • ユーザー ID に基づいて AuthorizationCodeFlow.loadCredential(String) を呼び出し、ユーザーの認証情報がすでにわかっているかどうかを確認します。動作すれば完了です。
  • 承認されていない場合は、AuthorizationCodeFlow.newAuthorizationUrl() を呼び出し、エンドユーザーのブラウザから認証ページにユーザーを誘導します。ここで、認証ページには、アプリケーションによる保護されたデータへのアクセスを許可できます。
  • その後、ウェブブラウザは「code」クエリ パラメータを使用してリダイレクト 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 メソッドを実装します。Google App Engine Data Store API を使用して認証情報を保持する場合は、AppEngineDataStoreFactoryJava 用 Google HTTP クライアント ライブラリで作成)を使用することをおすすめします。

サンプルコード:

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 を呼び出す必要があります。