概要
目的: このドキュメントでは、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(google-oauth-client-servlet から)
- com.google.api.client.extensions.appengine.auth.oauth2 (from google-oauth-client-appengine)
クライアントの登録
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 クライアント ライブラリで提供される StoredCredential で DataStoreFactory インターフェースと DataStore インターフェースを使用することもできます。
ライブラリで提供されている次のいずれかの実装を使用できます。
- JdoDataStoreFactory は、JDO を使用して認証情報を保持します。
- AppEngineDataStoreFactory は、Google App Engine Data Store API を使用して認証情報を保持します。
- MemoryDataStoreFactory は、認証情報をメモリに「保持」します。これは、プロセスの存続期間の短期ストレージとしてのみ有用です。
- FileDataStoreFactory は、認証情報をファイルに保持します。
Google App Engine ユーザー:
AppEngineCredentialStore は非推奨となり、削除される予定です。
StoredCredential で AppEngineDataStoreFactory を使用することをおすすめします。認証情報が古い方法で保存されている場合は、追加されたヘルパー メソッド 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 を使用していない場合は、下位レベルのクラスを使用することもできます。
- 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
メソッドを実装します。AppEngineDataStoreFactory(Java 用 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 を呼び出す必要があります。