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 とシークレットを取得するためのセルフサービス システムがあります。

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 を含む AccountManagerFutureOnTokenAcquiredrun() を呼び出します。呼び出しが成功した場合、トークンは 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 で受け取る Bundle(前の例の OnTokenAcquired)を介して通知されます。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 回リクエストする必要がなくなります。