FIDO2 から認証情報マネージャーに移行する

パスキー、フェデレーション ログイン、サードパーティ認証プロバイダをサポートする認証情報マネージャーは、Android での認証において推奨される API です。これを使用すると、安全かつ便利な環境でユーザーは認証情報の同期と管理ができます。ローカルの FIDO2 認証情報を使用するデベロッパーは、認証情報マネージャー API と統合して、パスキー認証をサポートするようにアプリを更新する必要があります。このドキュメントでは、プロジェクトを FIDO2 から認証情報マネージャーに移行する方法について説明します。

FIDO2 から認証情報マネージャーに移行する理由

ほとんどの場合、Android アプリの認証プロバイダを認証情報マネージャーに移行する必要があります。認証情報マネージャーに移行すべき理由は次のとおりです。

  • パスキーのサポート: 認証情報マネージャーは、パスワードよりも安全で使いやすい新しいパスワード不要の認証メカニズムである、パスキーをサポートしています。
  • 複数のログイン方法: 認証情報マネージャーは、パスワード、パスキー、フェデレーション ログインなど、複数のログイン方法をサポートしています。これにより、ユーザーは優先する認証方法に関係なく、簡単にアプリの認証を行うことができます。
  • サードパーティ認証情報プロバイダのサポート: Android 14 以上の認証情報マネージャーは複数のサードパーティ認証情報プロバイダをサポートしています。つまり、ユーザーは他のプロバイダの既存の認証情報を使用してアプリにログインできるということです。
  • 一貫したユーザー エクスペリエンス: 認証情報マネージャーにより、アプリやログイン メカニズムの違いにかかわらず、一貫した認証ユーザー エクスペリエンスが実現します。これにより、アプリの認証フローがわかりやすく使いやすいものになります。

FIDO2 から認証情報マネージャーへの移行は、次のようにして開始します。

依存関係を更新する

  1. プロジェクトの build.gradle の Kotlin プラグインをバージョン 1.8.10 以上に更新します。

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. プロジェクトの build.gradle で、認証情報マネージャーと Play 開発者サービス認証ライブラリの最新バージョンを使用するように依存関係を更新します。

      dependencies {
        // ...
        // Credential Manager:
        implementation 'androidx.credentials:credentials:<latest-version>'
    
        // Play Services Authentication:
        // Optional - needed for credentials support from play services, for devices running
        // Android 13 and below:
        implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
        // ...
      }
    
  3. FIDO の初期化を認証情報マネージャーの初期化に変更します。パスキーの作成とログイン方法に使用するクラスに、次の宣言を追加します。

    val credMan = CredentialManager.create(context)
    

パスキーを作成する

ユーザーがパスキーを使用してログインできるようにするには、新しいパスキーを作成してユーザーのアカウントに関連付け、パスキーの公開鍵をサーバーに保存する必要があります。登録関数の呼び出しを更新して、この機能をアプリにセットアップします。

図 1.認証情報マネージャーを使用してパスキーが作成された場合のアプリとサーバー間でのデータ交換方法を示した図
  1. パスキーの作成時に createCredential() メソッドに送信される必要なパラメータを取得するには、WebAuthn の仕様での説明のとおり、name("residentKey").value("required")registerRequest() サーバー呼び出しに追加します。

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("residentKey").value("required")
                    }
            }).build()
        )
        // ...
    }
    
  2. registerRequest()return タイプとすべての子関数を JSONObject に設定します。

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("authenticatorAttachment").value("platform")
                        name("userVerification").value("required")
                        name("residentKey").value("required")
                    }
                }).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. インテント ランチャーとアクティビティ結果の呼び出しを処理するメソッドをビューから安全に削除します。

  4. registerRequest()JSONObject を返すようになるため、PendingIntent を作成する必要はありません。返されたインテントを JSONObject に置き換えます。認証情報マネージャー API から createCredential() を呼び出すように、インテント ランチャーの呼び出しを更新します。createCredential() API メソッドを呼び出します。

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. 呼び出しが正常に完了したら、サーバーにレスポンスを返します。この呼び出しのリクエストとレスポンスは FIDO2 の実装に類似しているため、変更する必要はありません。

パスキーで認証する

パスキーの作成をセットアップ後、ユーザーがパスキーを使用してログインと認証ができるようにアプリをセットアップします。そのためには、認証コードを更新して認証情報マネージャーの結果を処理するようにし、パスキーで認証する関数を実装します。

図 2.認証情報マネージャーでのパスキーによる認証フロー
  1. getCredential() リクエストに送信される必要な情報を取得するためのサーバーへのログイン リクエストの呼び出しは、FIDO2 実装と同じです。そのため、変更は不要です。
  2. 登録リクエストの呼び出しと同様に、JSONObject 形式のレスポンスが返されます。

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(Builder().url(buildString {
            append("$BASE_URL/signinRequest")
        }).method("POST", jsonRequestBody {})
            .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String, response: JSONObject, credentialId: String
    ): ApiResult<Unit> {
    
        val call = client.newCall(
            Builder().url("$BASE_URL/signinResponse")
                .addHeader("Cookie",formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("id").value(credentialId)
                    name("type").value(PUBLIC_KEY.toString())
                    name("rawId").value(credentialId)
                    name("response").objectValue {
                        name("clientDataJSON").value(
                            response.getString("clientDataJSON")
                        )
                        name("authenticatorData").value(
                            response.getString("authenticatorData")
                        )
                        name("signature").value(
                            response.getString("signature")
                        )
                        name("userHandle").value(
                            response.getString("userHandle")
                        )
                    }
                }).build()
        )
        val apiResponse = call.await()
        return apiResponse.result("Error calling /signingResponse") {
        }
    }
    
  3. インテント ランチャーとアクティビティ結果の呼び出しを処理するメソッドをビューから安全に削除します。

  4. signInRequest()JSONObject を返すようになるため、PendingIntent を作成する必要はありません。返されたインテントを JSONObject に置き換え、API メソッドから getCredential() を呼び出します。

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
    ): GetCredentialResponse? {
        Toast.makeText(
            activity,
            "Fetching previously stored credentials",
            Toast.LENGTH_SHORT)
            .show()
        var result: GetCredentialResponse? = null
        try {
            val request= GetCredentialRequest(
                listOf(
                    GetPublicKeyCredentialOption(
                        creationResult.toString(),
                        null
                    ),
                    GetPasswordOption()
                )
            )
            result = credMan.getCredential(activity, request)
            if (result.credential is PublicKeyCredential) {
                val publicKeycredential = result.credential as PublicKeyCredential
                Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                return result
            }
        } catch (e: Exception) {
            showErrorAlert(activity, e)
        }
        return result
    }
    
  5. 呼び出しが正常に完了したら、サーバーにレスポンスを返し、ユーザーの検証と認証を行います。この API 呼び出しのリクエスト パラメータとレスポンス パラメータは FIDO2 実装と同様であるため、変更する必要はありません。

参考情報