Java 用 OAuth 2.0 と 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 実装は非推奨であり、今後のリリースで削除される予定です。別の方法として、DataStoreFactory インターフェースと DataStore インターフェースを StoredCredential とともに使用して、Java 用 Google HTTP クライアント ライブラリを使用することもできます。

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

Google App Engine ユーザー:

AppEngineCredentialStore は非推奨であり、削除中です。

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

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

認証コードフロー

認証コードフローを使用して、エンドユーザーが保護されたデータをアプリケーションに付与できるようにします。このフローのプロトコルは認証コード付与の仕様に記載されています。

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

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

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

サーブレット認証コードフロー

このライブラリは、基本的なユースケースの認証コードフローを大幅に簡素化するためのサーブレット ヘルパークラスを提供します。AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServlet の具体的なサブクラス(google-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)をご覧ください。

サーブレットの場合との主な違いは、AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet の具体的なサブクラス(google-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 を呼び出す必要があります。