OAuth2 サービスの認証

認証トークンのロジック図
図 1. Android アカウント マネージャーから有効な認証トークンを取得する手順

オンライン サービスに安全にアクセスするには、ユーザーがサービスに認証される必要があります。つまり、ユーザーは身元の証明を提出する必要があります。サードパーティのサービスにアクセスするアプリケーションの場合、セキュリティの問題はさらに複雑です。ユーザーがサービスにアクセスするための認証を得る必要があるだけでなく、アプリケーションがユーザーの代理として動作することの承認を得る必要もあります。

サードパーティ サービスへの認証を処理する業界標準の方法は、OAuth2 プロトコルです。OAuth2 では、認証トークンと呼ばれる単一の値が提供されます。これは、ユーザーの ID と、ユーザーの代理として動作するアプリケーションの承認の両方を表します。このレッスンでは、OAuth2 をサポートする Google サーバーへの接続について説明します。Google サービスが例として使用されていますが、ここで示されている手法は、OAuth2 プロトコルを正しくサポートするすべてのサービスで機能します。

OAuth2 は次の場合に適しています。

  • ユーザーのアカウントを使用してオンライン サービスにアクセスする許可をそのユーザーから得る。
  • ユーザーに代わってオンライン サービスへの認証を得る。
  • 認証エラーを処理する。

情報を入手する

OAuth2 を使用するにあたって、アクセスする API に関する情報がいくつか必要です。

  • アクセスするサービスの URL
  • 認証範囲。アプリが要求するアクセスの種類を定義する文字列です。たとえば、Google ToDo リストの読み取り専用アクセス権の認証範囲は「View your tasks」、Google ToDo リストの読み取り / 書き込みアクセス権の認証範囲は「Manage your tasks」です。
  • クライアント IDクライアント シークレット。サービスに対してアプリを識別する文字列です。これらの文字列は、サービス所有者から直接取得する必要があります。Google では、クライアント ID とシークレットをご自身で取得するシステムを提供しています。Google ToDo リスト API で使用するこれらの値の取得方法については、REST API の認証と使用の記事をご覧ください。

INTERNET パーミッションをリクエストする

Android 6.0(API レベル 23)以降を対象とするアプリでは、getAuthToken() メソッド自体にパーミッションは必要ありません。しかし、トークンに対して操作を実行するには、次のコード スニペットに示すように、INTERNET パーミッションをマニフェスト ファイルに追加する必要があります。

    <manifest ... >
        <uses-permission android:name="android.permission.INTERNET" />
        ...
    </manifest>
    

認証トークンをリクエストする

このトークンを取得するには、AccountManager.getAuthToken() を呼び出します。

注意: アカウント操作ではネットワーク通信が必要になる場合があるため、AccountManager のメソッドのほとんどは非同期です。つまり、すべての認証処理を 1 つの関数で行うのではなく、一連のコールバックとして実装する必要があります。

次のスニペットは、一連のコールバックを使用してトークンを取得する方法を示しています。

Kotlin

    val am: AccountManager = AccountManager.get(this)
    val options = Bundle()

    am.getAuthToken(
            myAccount_,                     // Account retrieved using getAccountsByType()
            "Manage your tasks",            // Auth scope
            options,                        // Authenticator-specific options
            this,                           // Your activity
            OnTokenAcquired(),              // Callback called when a token is successfully acquired
            Handler(OnError())              // Callback called if an error occurs
    )
    

Java

    AccountManager am = AccountManager.get(this);
    Bundle options = new Bundle();

    am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        new OnTokenAcquired(),          // Callback called when a token is successfully acquired
        new Handler(new OnError()));    // Callback called if an error occurs
    

この例の OnTokenAcquiredAccountManagerCallback を実装するクラスです。AccountManager は、Bundle を含んだ AccountManagerFuture を引数として、OnTokenAcquiredrun() を呼び出します。この呼び出しが成功した場合、Bundle の中にトークンが返されます。

次のようにして Bundle からトークンを取得します。

Kotlin

    private class OnTokenAcquired : AccountManagerCallback<Bundle> {

        override fun run(result: AccountManagerFuture<Bundle>) {
            // Get the result of the operation from the AccountManagerFuture.
            val bundle: Bundle = result.getResult()

            // The token is a named value in the bundle. The name of the value
            // is stored in the constant AccountManager.KEY_AUTHTOKEN.
            val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
        }
    }
    

Java

    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            // Get the result of the operation from the AccountManagerFuture.
            Bundle bundle = result.getResult();

            // The token is a named value in the bundle. The name of the value
            // is stored in the constant AccountManager.KEY_AUTHTOKEN.
            String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
            ...
        }
    }
    

問題がなければ、BundleKEY_AUTHTOKEN キーに有効なトークンが格納され、次の段階に進むことができます。しかし、そのようにスムーズに進むとは限りません。

認証トークンをリクエストする(もう一度)

認証トークンの最初のリクエストが失敗する理由には、次のようなものがあります。

  • デバイスまたはネットワークの問題により、AccountManager でエラーが発生した。
  • アプリがアカウントにアクセスすることをユーザーが許可しなかった。
  • 保存されていたアカウントの認証情報では、アカウントにアクセスできない。
  • キャッシュに残っていた認証トークンの有効期限が切れている。

通常、アプリケーションで最初の 2 つを処理するのは簡単で、ユーザーにエラーメッセージを表示するだけです。ネットワークがダウンしている場合や、ユーザーがアクセスを許可しない場合に、アプリケーションでできることは多くありません。最後の 2 つのケースはもう少し複雑です。適切に動作するアプリケーションならば、このようなエラーを自動的に処理することが期待されるためです。

3 番目の認証情報が不十分なケースでは、AccountManagerCallback で(前の例では OnTokenAcquired から)受け取った Bundle を介して情報が得られます。BundleKEY_INTENT キーに Intent が含まれている場合、認証システムが、有効なトークンを与える前にユーザーと直接やり取りする必要があることを示しています。

認証システムが Intent を返す理由はさまざまです。ユーザーがこのアカウントに初めてログインしたのかもしれません。ユーザーのアカウントの有効期限が切れていて、再度ログインする必要がある場合もあります。アカウントで 2 要素認証を必要としている可能性や、網膜スキャンのためにカメラを起動する必要がある可能性もあります。理由は問題ではありません。有効なトークンが必要な場合は、Intent を送信する必要があります。

Kotlin

    private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

        override fun run(result: AccountManagerFuture<Bundle>) {
            val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
            if (launch != null) {
                startActivityForResult(launch, 0)
            }
        }
    }
    

Java

    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            ...
            Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
            if (launch != null) {
                startActivityForResult(launch, 0);
                return;
            }
        }
    }
    

この例では startActivityForResult() を使用しているため、独自のアクティビティに onActivityResult() を実装して Intent の結果を取得できます。これは重要な手順です。認証システムのレスポンス Intent から結果を取得しない場合、ユーザーが正常に認証されたかどうかを判断できません。結果が RESULT_OK の場合、認証システムはリクエストされたアクセスレベルを満たす認証情報で保存済みの認証情報を更新しているので、AccountManager.getAuthToken() を再度呼び出して新しい認証トークンをリクエストする必要があります。

最後に、トークンが期限切れになっているケースについて説明すると、実際には AccountManager のエラーではありません。トークンが期限切れであるかどうかを確認する唯一の方法は、サーバーに接続することですが、AccountManager がすべてのトークンの状態を確認するために頻繁に接続するのは無駄でコストが掛かります。そのためこのエラーは、次の例のアプリケーションのように認証トークンを使用してオンライン サービスにアクセスするときにのみ検出できます。

オンライン サービスに接続する

次の例は、Google サーバーに接続する方法を示しています。Google は業界標準の OAuth2 プロトコルを使用してリクエストを認証するため、ここで説明する手法は広く適用可能です。ただし、すべてのサーバーが異なることに注意してください。以下の手順は、個々の状況に応じて、少しの変更が必要になる場合があります。

Google API では、各リクエストに API キー、クライアント ID、クライアント シークレット、認証キーの 4 つの値を指定する必要があります。最初の 3 つは Google API Console ウェブサイトから取得したものです。最後は、AccountManager.getAuthToken() を呼び出して取得した文字列値です。これらを HTTP リクエストの一部として Google サーバーに渡します。

Kotlin

    val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
    val conn = url.openConnection() as HttpURLConnection
    conn.apply {
        addRequestProperty("client_id", your client id)
        addRequestProperty("client_secret", your client secret)
        setRequestProperty("Authorization", "OAuth $token")
    }
    

Java

    URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
    URLConnection conn = (HttpURLConnection) url.openConnection();
    conn.addRequestProperty("client_id", your client id);
    conn.addRequestProperty("client_secret", your client secret);
    conn.setRequestProperty("Authorization", "OAuth " + token);
    

リクエストが HTTP エラーコード 401 を返した場合、トークンは拒否されています。前のセクションで説明したように、最も一般的な原因はトークンの期限切れです。修正は簡単です。AccountManager.invalidateAuthToken() を呼び出して、もう一度トークン取得の手順を繰り返します。

トークンの期限切れはよくあることであり、修正は非常に簡単です。そのため、多くのアプリケーションはトークンを要求する前に、単にトークンの期限が切れていると仮定します。サーバーでのトークン更新のオペレーション コストが低い場合、最初に AccountManager.getAuthToken() を呼び出す前に AccountManager.invalidateAuthToken() を呼び出して、認証トークンを 2 回リクエストせずに済ませることもできます。