概要
目的: このドキュメントでは、Java 用 Google OAuth クライアント ライブラリが提供する一般的な OAuth 2.0 関数について説明します。これらの関数は、任意のインターネット サービスの認証と認可に使用できます。
GoogleCredential
を使用して Google サービスで OAuth 2.0 認証を行う手順については、Java 用 Google API クライアント ライブラリでの OAuth 2.0 の使用をご覧ください。
概要: OAuth 2.0 は、保護されたサーバー側リソースへのアクセスをエンドユーザーがクライアント アプリケーションを安全に承認するための標準仕様です。また、OAuth 2.0 署名なしトークン仕様では、エンドユーザーの承認プロセス中に付与されたアクセス トークンを使用して、これらの保護されたリソースにアクセスする方法について説明しています。
詳細については、次のパッケージの Javadoc ドキュメントをご覧ください。
- com.google.api.client.auth.oauth2(google-oauth-client)
- com.google.api.client.extensions.servlet.auth.oauth2 (from google-oauth-client-servlet)
- com.google.api.client.extensions.appengine.auth.oauth2 (from google-oauth-client-appengine)
クライアントの登録
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 クライアント ライブラリが提供する StoredCredential で DataStoreFactory インターフェースと DataStore インターフェースを使用することもできます。
ライブラリが提供する次のいずれかの実装を使用できます。
- JdoDataStoreFactory は、WebView を使用して認証情報を保持します。
- AppEngineDataStoreFactory は Google App Engine Data Store API を使用して認証情報を保持します。
- MemoryDataStoreFactory は認証情報をメモリに「保持」します。これは、プロセスの存続期間中の短期的なストレージとしてのみ有用です。
- FileDataStoreFactory は認証情報をファイル内に保持します。
Google App Engine ユーザー:
AppEngineCredentialStore は非推奨になり、今後削除されます。
AppEngineDataStoreFactory と StoredCredential を使用することをおすすめします。以前の方法で認証情報が保存されている場合は、追加されたヘルパー メソッド 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 を使用していない場合は、次のように下位レベルのクラスを使用することもできます。
- DataStore.get(String) を使用し、ユーザー ID に基づいてストアから認証情報を読み込みます。
- AuthorizationCodeRequestUrl を使用して、ブラウザを認証ページに移動させます。
- AuthorizationCodeResponseUrl を使用して、認可レスポンスを処理し、認可コードを解析します。
- AuthorizationCodeTokenRequest を使用して、アクセス トークン(場合によっては更新トークン)をリクエストします。
- 新しい認証情報を作成し、DataStore.set(String, V) を使用して保存します。
- 認証情報を使用して、保護されたリソースにアクセスします。期限切れのアクセス トークンは、更新トークンを使用して自動的に更新されます(該当する場合)。必ず DataStoreCredentialRefreshListener を使用し、Credential.Builder.addRefreshListener(CredentialRefreshListener) を使用して認証情報に設定します。
サーブレット認可コードフロー
このライブラリには、基本的なユースケースの認可コードフローを大幅に簡素化するためのサーブレット ヘルパークラスが用意されています。AbstractAuthorizationCodeServletと AbstractAuthorizationCodeCallbackServlet(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 内)をご覧ください。
サーブレットの場合との主な違いは、AbstractAppEngineAuthorizationCodeServletと AbstractAppEngineAuthorizationCodeCallbackServlet(google-oauth-client-appengine から取得)の具体的なサブクラスを指定する点です。抽象サーブレット クラスを拡張し、Users Java API を使用して getUserId
メソッドを実装します。Google App Engine Data Store API を使用して認証情報を保持する場合は、AppEngineDataStoreFactory(Java 用 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 を呼び出す必要があります。