認証情報マネージャーを認証情報プロバイダのソリューションと統合する

認証情報マネージャーは Android 14 で導入された API で、ユーザー名とパスワード、パスキー、フェデレーション ログイン ソリューション(Google でログインなど)といった複数のログイン方法をサポートしています。Credential Manager API が呼び出されると、Android システムは、デバイスにインストールされているすべての認証情報プロバイダから認証情報を集約します。このドキュメントでは、これらの認証情報プロバイダに統合エンドポイントを提供する一連の API について説明します。

セットアップ

認証情報プロバイダに機能を実装する前に、次のセクションで説明するセットアップ手順を完了します。

依存関係の宣言

モジュールの build.gradle ファイルで、認証情報マネージャー ライブラリの最新バージョンを使用して依存関係を宣言します。

implementation "androidx.credentials:credentials:1.2.0-{latest}"

マニフェスト ファイルでサービス要素を宣言する

アプリのマニフェスト ファイル AndroidManifest.xml で、以下の例のような androidx.credentials ライブラリから、CredentialProviderService クラスを拡張するサービスクラスの <service> 宣言を追加します。

<service android:name=".MyCredentialProviderService"
         android:enabled="true"
         android:exported="true"
         android:label="My Credential Provider"
         android:icon="<any drawable icon>"
         android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
         android:name="android.credentials.provider"
         android:resource="@xml/provider"/>
</service>

認証情報マネージャーのフローが想定どおりに機能するには、上記の権限とインテント フィルタが不可欠です。このサービスに Android システムのみをバインドするためには、この権限が必要です。インテント フィルタは、認証情報マネージャーが使用する認証情報プロバイダとして、このサービスを検出するために使用されます。

サポートされている認証情報タイプを宣言する

res/xml ディレクトリに provider.xml という名前の新しいファイルを作成します。このファイルでは、ライブラリの各認証情報タイプに対して定義された定数を使用して、サービスがサポートする認証情報タイプを宣言します。次の例では、サービスは従来のパスワードとパスキーをサポートしており、その定数は、TYPE_PASSWORD_CREDENTIALTYPE_PUBLIC_KEY_CREDENTIAL として定義されています。

<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
   <capabilities>
       <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
       <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
   </capabilities>
</credential-provider>

以前の API レベルでは、認証情報プロバイダはパスワードやその他データの自動入力などの API と統合されています。これらのプロバイダは、同じ内部インフラストラクチャを使用して既存の認証情報タイプを保存し、それを拡張してパスキーなど他の認証情報をサポートできます。

プロバイダとのやり取りに対する 2 段階のアプローチ

認証情報マネージャーは、次の 2 つのフェーズで認証情報プロバイダとやり取りします。

  1. 最初のフェーズは開始 / クエリフェーズです。このフェーズでは、システムが認証情報プロバイダのサービスにバインドされ、onBeginGetCredentialRequest() メソッド、onBeginCreateCredentialRequest() メソッド、または Begin… リクエストを持つ onClearCredentialStateRequest() メソッドを呼び出します。プロバイダは、これらのリクエストを処理し、Begin… レスポンスで応答する必要があります。その結果、アカウント セレクタに表示されるビジュアル オプションを表すエントリが入力されます。各エントリには PendingIntent を設定する必要があります。
  2. ユーザーがエントリを選択すると、選択フェーズが開始され、エントリに関連付けられた PendingIntent とともに対応するプロバイダのアクティビティが起動します。ユーザーがこのアクティビティの操作を完了したら、認証情報プロバイダは、アクティビティを終了する前に、操作の結果に対するレスポンスを設定する必要があります。このレスポンスは、認証情報マネージャーを呼び出したクライアント アプリに送信されます。

パスキーの作成を処理する

パスキー作成のクエリを処理する

クライアント アプリがパスキーを作成して認証情報プロバイダに保存する場合は、createCredential API を呼び出します。パスキーが実際にストレージに保存されるように、認証情報プロバイダのサービスでこのリクエストを処理するには、次のセクションの手順を実施します。

  1. CredentialProviderService から拡張されたサービスの onBeginCreateCredentialRequest() メソッドをオーバーライドします。
  2. BeginCreateCredentialRequest を処理するには、対応する BeginCreateCredentialResponse を作成し、コールバックを介してそれを渡します。
  3. BeginCreateCredentialResponse を作成する際に、必要な CreateEntries を追加します。各 CreateEntry は、認証情報を保存できるアカウントに対応しており、PendingIntent が他の必須のメタデータとともに設定されている必要があります。

次の例は、この手順を実行する方法を示しています。

override fun onBeginCreateCredentialRequest(
  request: BeginCreateCredentialRequest,
  cancellationSignal: CancellationSignal,
  callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
  val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
  if (response != null) {
    callback.onResult(response)
  } else {
    callback.onError(CreateCredentialUnknownException())
  }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }
  }
  // Request not supported
  return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
    ): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add( CreateEntry(
        PERSONAL_ACCOUNT_ID,
        createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    createEntries.add( CreateEntry(
        FAMILY_ACCOUNT_ID,
        createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
        )
    )
}

PendingIntent の構造は、次のことを遵守する必要があります。

  • 必要な生体認証プロンプト、確認、選択を表示するために、対応するアクティビティを設定する。
  • 対応するアクティビティが呼び出されたときにプロバイダが必要とするデータは、作成フローの accountId など、PendingIntent の作成に使用するインテントでエクストラとして設定する。
  • システムが最終リクエストをインテント エクストラに追加できるように、PendingIntentPendingIntent.FLAG_MUTABLE フラグで作成する。
  • ユーザーがエントリを選択し、戻って再度選択すると PendingIntent が 2 回呼び出される可能性があるため、PendingIntent.FLAG_ONE_SHOT フラグで PendingIntent を作成しない。
  • 各エントリが対応する PendingIntent を持つことができるように、PendingIntent は一意のリクエスト コードで構築する。

パスキー作成リクエストのエントリ選択を処理する

  1. ユーザーが以前に入力された CreateEntry を選択すると、対応する PendingIntent が呼び出され、関連するプロバイダ Activity が作成されます。
  2. アクティビティの onCreate メソッドが呼び出されたら、関連するインテントにアクセスし、それを PendingIntentHander クラスに渡して ProviderCreateCredentialRequest を取得します。
  3. リクエストから requestJsoncallingAppInfoclientDataHash を抽出します。
  4. インテント エクストラからローカルの accountId を抽出します。これはサンプルアプリ固有の実装であり、必須ではありません。このアカウント ID は、この特定のアカウント ID の認証情報を保存するのに使用できます。
  5. requestJson を検証します。次の例では、PublicKeyCredentialCreationOptions などのローカル データクラスを使用して、入力 JSON を WebAuthn の仕様に即した構造化クラスに変換します。認証情報プロバイダは、これを独自のパーサーに置き換えることができます。
  6. 呼び出しがネイティブな Android アプリからのものである場合、呼び出し元アプリの asset-link を確認します。
  7. 認証プロンプトを表示します。以下の例では、Android Biometric API を使用しています。
  8. 認証が成功したら、credentialId鍵ペアを生成します。
  9. callingAppInfo.packageName に対する秘密鍵をローカル データベースに保存します。
  10. 公開鍵と、credentialId で構成される Web Authentication API JSON レスポンスを作成します。以下の例では、AuthenticatorAttestationResponseFidoPublicKeyCredential といったローカルのユーティリティ クラスを使用しており、これは前述の仕様に基づいて JSON を構築するのに役立ちます。認証情報プロバイダは、これらのクラスを独自のビルダーに置き換えることができます。
  11. 上記で生成した JSON を使用して CreatePublicKeyCredentialResponse を作成します。
  12. PendingIntentHander.setCreateCredentialResponse() からの CreatePublicKeyCredentialResponse を エクストラとして Intent に設定し、そのインテントをアクティビティの結果に設定します。
  13. アクティビティを終了します。

以下のコード例は、これらのステップを示しています。onCreate() が呼び出されたら、このコードをアクティビティ クラスで処理する必要があります。

val request =
  PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
  val publicKeyRequest: CreatePublicKeyCredentialRequest =
    request.callingRequest as CreatePublicKeyCredentialRequest
  createPasskey(
    publicKeyRequest.requestJson,
    request.callingAppInfo,
    publicKeyRequest.clientDataHash,
    accountId
  )
}

fun createPasskey(
  requestJson: String,
  callingAppInfo: CallingAppInfo?,
  clientDataHash: ByteArray?,
  accountId: String?
) {
  val request = PublicKeyCredentialCreationOptions(requestJson)

  val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
      override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
      ) {
        super.onAuthenticationError(errorCode, errString)
        finish()
      }

      override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
        finish()
      }

      override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
      ) {
        super.onAuthenticationSucceeded(result)

        // Generate a credentialId
        val credentialId = ByteArray(32)
        SecureRandom().nextBytes(credentialId)

        // Generate a credential key pair
        val spec = ECGenParameterSpec("secp256r1")
        val keyPairGen = KeyPairGenerator.getInstance("EC");
        keyPairGen.initialize(spec)
        val keyPair = keyPairGen.genKeyPair()

        // Save passkey in your database as per your own implementation

        // Create AuthenticatorAttestationResponse object to pass to
        // FidoPublicKeyCredential

        val response = AuthenticatorAttestationResponse(
          requestOptions = request,
          credentialId = credentialId,
          credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
          origin = appInfoToOrigin(callingAppInfo),
          up = true,
          uv = true,
          be = true,
          bs = true,
          packageName = callingAppInfo.packageName
        )

        val credential = FidoPublicKeyCredential(
          rawId = credentialId, response = response
        )
        val result = Intent()

        val createPublicKeyCredResponse =
          CreatePublicKeyCredentialResponse(credential.json())

        // Set the CreateCredentialResponse as the result of the Activity
        PendingIntentHandler.setCreateCredentialResponse(
          result, createPublicKeyCredResponse
        )
        setResult(Activity.RESULT_OK, result)
        finish()
      }
    }
  )

  val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Create passkey for ${request.rp.name}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
      )
    .build()
  biometricPrompt.authenticate(promptInfo)
}

fun appInfoToOrigin(info: CallingAppInfo): String {
  val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
  val md = MessageDigest.getInstance("SHA-256");
  val certHash = md.digest(cert)
  // This is the format for origin
  return "android:apk-key-hash:${b64Encode(certHash)}"
}

パスワード作成リクエストのクエリを処理する

パスワード作成リクエストのクエリを処理する手順は次のとおりです。

  • 前のセクションで説明した processCreateCredentialRequest() メソッド内で、パスワード リクエストを処理するためのスイッチ ブロック内に別のケースを追加します。
  • BeginCreateCredentialResponse を作成する際に、必要な CreateEntries を追加します。
  • CreateEntry は、認証情報を保存できるアカウントに対応しており、他のメタデータとともに PendingIntent が設定されている必要があります。

次の例は、この手順を実行する方法を示しています。

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
  ): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }

    is BeginCreatePasswordCredentialRequest -> {
    // Request is password type
      return handleCreatePasswordQuery(request)
    }
  }
  return null
}

private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
  ): BeginCreateCredentialResponse {
  val createEntries: MutableList<CreateEntry> = mutableListOf()

  // Adding two create entries - one for storing credentials to the 'Personal'
  // account, and one for storing them to the 'Family' account. These
  // accounts are local to this sample app only.
  createEntries.add(
    CreateEntry(
      PERSONAL_ACCOUNT_ID,
      createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )
  createEntries.add(
    CreateEntry(
      FAMILY_ACCOUNT_ID,
      createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )

  return BeginCreateCredentialResponse(createEntries)
}

パスワード作成リクエストのエントリ選択を処理する

入力された CreateEntry をユーザーが選択すると、対応する PendingIntent が実行され、関連するアクティビティが表示されます。onCreate で渡された関連インテントにアクセスし、それを PendingIntentHander クラスに渡して ProviderCreateCredentialRequest メソッドを取得します。

以下に、このプロセスを実装する例を示します。このコードはアクティビティの onCreate() メソッドで処理する必要があります。

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
<your_database>.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

//Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
this@<activity>.finish()

ユーザーのログインを処理する

ユーザーのログインは次の手順で処理されます。

  • クライアント アプリがユーザーのログインを試みると、GetCredentialRequest インスタンスが作成されます。
  • Android フレームワークは、これらのサービスにバインドすることで、該当するすべての認証情報プロバイダにこのリクエストを伝えます。
  • その後、プロバイダ サービスは、BeginGetCredentialOption のリストを含む BeginGetCredentialRequest を受け取ります。それぞれに一致する認証情報の取得に使用できるパラメータが含まれます。

このリクエストを認証情報プロバイダ サービスで処理する手順は次のとおりです。

  1. onBeginGetCredentialRequest() メソッドをオーバーライドして、リクエストを処理します。認証情報がロックされている場合は、すぐにレスポンスに AuthenticationAction を設定してコールバックを呼び出すことができます。

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(BeginGetCredentialResponse(
                authenticationActions = mutableListOf(AuthenticationAction(
                    unlockEntryTitle, createUnlockPendingIntent())
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

    credentialEntries を返す前に認証情報のロック解除が必要なプロバイダは、ユーザーをアプリのロック解除フローに誘導するペンディング インテントを設定する必要があります。

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQUEST_CODE, intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
            )
        )
    }
    
  2. ローカル データベースから認証情報を取得し、CredentialEntries を使用してセレクタに表示されるように設定します。パスキーの場合、credentialId をエクストラとしてインテントに設定することで、ユーザーがこのエントリを選択したときにどの認証情報にマッピングされるかを把握できます。

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    
    }
    
    fun processGetCredentialsRequest(
    request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackage = request.callingAppInfo?.packageName
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                            populatePasswordData(
                                callingPackage,
                                option
                            )
                        )
                    }
                    is BeginGetPublicKeyCredentialOption -> {
                        credentialEntries.addAll(
                            populatePasskeyData(
                                callingPackage,
                                option
                            )
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. データベースから認証情報をクエリし、パスキーとパスワード エントリを作成して入力します。

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
      val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
      val request = PublicKeyCredentialRequestOptions(option.requestJson)
      // Get your credentials from database where you saved during creation flow
      val creds = <getCredentialsFromInternalDb(request.rpId)>
      val passkeys = creds.passkeys
      for (passkey in passkeys) {
          val data = Bundle()
          data.putString("credId", passkey.credId)
          passkeyEntries.add(
              PublicKeyCredentialEntry(
                  context = applicationContext,
                  username = passkey.username,
                  pendingIntent = createNewPendingIntent(
                      GET_PASSKEY_INTENT_ACTION,
                      data
                  ),
                  beginPublicKeyCredentialOption = option,
                  displayName = passkey.displayName,
                  icon = passkey.icon
              )
          )
      }
      return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to
    // the user
    private fun populatePasswordData(
    callingPackage: String,
    option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = <getCredentialsFromInternalDb(callingPackage)>
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                    GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option
                        displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. 認証情報をクエリして入力したら、ユーザーが選択した認証情報の選択フェーズ(パスキーかパスワードか)を処理する必要があります。

パスキーのユーザー選択の処理

  1. 対応するアクティビティの onCreate メソッドで、関連するインテントを取得して、PendingIntentHandler.retrieveProviderGetCredentialRequest() に渡します。
  2. 上記で取得したリクエストから GetPublicKeyCredentialOption を抽出します。次に、このオプションから requestJsonclientDataHash を抽出します。
  3. 対応する PendingIntent の設定時に認証情報プロバイダによって入力されたインテント エクストラから、credentialId を抽出します。
  4. 上記でアクセスしたリクエスト パラメータを使用して、ローカル データベースからパスキーを抽出します。
  5. 抽出されたメタデータとユーザー検証で、パスキーが有効であることをアサートします。

    val getRequest =
        PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest =
    getRequest.credentialOption as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo.getString("credId")
    
    // Get the saved passkey from your database based on the credential ID
    // from the publickeyRequest
    val passkey = <your database>.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. ユーザーを検証するには、生体認証プロンプト(または他のアサーション メソッド)を表示します。以下のコード スニペットでは、Android Biometric API を使用しています。

  7. 認証が成功したら、W3 Web Authentication Assertion 仕様に基づいて JSON レスポンスを作成します。以下のコード スニペットでは、AuthenticatorAssertionResponse などのヘルパーデータ クラスを使用して、構造化パラメータを取得し、必要な JSON 形式に変換しています。レスポンスには、WebAuthn 認証情報の秘密鍵からのデジタル署名が含まれます。リライング パーティのサーバーは、この署名を検証してログイン前にユーザーを認証できます。

  8. 上で生成した JSON を使用して PublicKeyCredential を作成し、最終的な GetCredentialResponse に設定します。このアクティビティの結果に対して、この最終レスポンスを設定します。

次の例は、これらのステップを実行する方法を示しています。

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
        ) {
        super.onAuthenticationSucceeded(result)
        val response = AuthenticatorAssertionResponse(
            requestOptions = request,
            credentialId = credId,
            origin = origin,
            up = true,
            uv = true,
            be = true,
            bs = true,
            userHandle = uid,
            packageName = packageName
        )

        val sig = Signature.getInstance("SHA256withECDSA");
        sig.initSign(privateKey)
        sig.update(response.dataToSign())
        response.signature = sig.sign()

        val credential = FidoPublicKeyCredential(
            rawId = credId, response = response
        )
        val result = Intent()
        val passkeyCredential = PublicKeyCredential(credential.json)
        PendingIntentHandler.setGetCredentialResponse(
            result, GetCredentialResponse(passkeyCredential)
        )
        setResult(RESULT_OK, result)
        finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
    .build()
biometricPrompt.authenticate(promptInfo)

パスワード認証のユーザー選択の処理

  1. 対応するアクティビティで、onCreate に渡されたインテントにアクセスし、PendingIntentHandler を使用して ProviderGetCredentialRequest を抽出します。
  2. 受信パッケージ名のパスワード認証情報を取得するには、リクエストで GetPasswordOption を使用します。

    val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest.credentialOption as GetPasswordCredentialOption
    
    val username = passwordOption.username
    // Fetch the credentials for the calling app package name
    val creds = <your_database>.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext() == true) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
           password = passwordItemCurrent.password
           break
        }
    }
    
  3. 取得したら、選択したパスワード認証情報のレスポンスを設定します。

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
    result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

認証アクション エントリの選択を処理する

前述のように、認証情報プロバイダがロックされている場合、認証情報プロバイダで AuthenticationAction を設定できます。ユーザーがこのエントリを選択すると、PendingIntent に設定されたインテント アクションに対応するアクティビティが呼び出されます。認証情報プロバイダは、生体認証フローまたは同様のメカニズムを表示して、認証情報のロックを解除できます。認証情報のロックが解除されると、認証情報プロバイダは、上記のユーザー ログイン処理と同様にBeginGetCredentialResponse を作成する必要があります。PendingIntentHandler.setBeginGetCredentialResponse() メソッドでこのレスポンスを設定してから、準備済みのインテントを結果として設定し、アクティビティを終了します。

認証情報リクエストを消去する

クライアント アプリは、認証情報の選択のために維持している状態をクリアするようリクエストできます。たとえば、認証情報プロバイダは以前に選択された認証情報を記憶していて、次回にその状態のみを返すことがあります。クライアント アプリは、この API を呼び出して、固定されている選択がクリアされることを想定しています。認証情報プロバイダのサービスは、onClearCredentialStateRequest() メソッドをオーバーライドすることで、このリクエストを処理できます。

override fun onClearCredentialStateRequest(
    request: android.service.credentials.ClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>,
  ) {
    // Delete any maintained state as appropriate.
}

ユーザーが [パスワード、パスキー、自動入力] 画面からプロバイダの設定を開けるようにするには、認証情報プロバイダ アプリで res/xml/provider.xmlcredential-provider settingsActivity マニフェスト属性を実装する必要があります。この属性を使用すると、ユーザーがサービスの [パスワード、パスキー、自動入力] リストでプロバイダ名をクリックしたときに、インテントを使用してアプリ独自の設定画面を開くことができます。この属性の値は、設定画面から起動するアクティビティの名前に設定します。

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
「変更」ボタンと「開く」ボタンの機能を示す図
図 1: [変更] ボタンをクリックすると、既存の選択ダイアログが開き、ユーザーは使用する認証情報プロバイダを選択できます。[開く] ボタンは、マニフェストの変更で定義された設定アクティビティを起動し、そのプロバイダ専用の設定ページを開きます。

設定インテント

設定を開く: android.settings.CREDENTIAL_PROVIDER インテントを実行すると、設定画面が表示され、ユーザーは優先する認証情報プロバイダと追加の認証情報プロバイダを選択できます。

[パスワード、パスキー、自動入力] 設定画面
図 2: [パスワード、パスキー、自動入力] の設定画面。

優先認証情報サービス: ACTION_REQUEST_SET_AUTOFILL_SERVICE インテントは、優先プロバイダ選択画面にユーザーをリダイレクトします。この画面で選択したプロバイダが、優先認証情報と自動入力プロバイダになります。

変更ボタンと開くボタンの機能を示す図
図 3: パスワード、パスキー、自動入力で優先的に使用するサービス設定画面

特権アプリの許可リストを取得する

ウェブブラウザなどの特権アプリは、認証情報マネージャーの GetCredentialRequest() メソッドと CreatePublicKeyCredentialRequest() メソッドで origin パラメータを設定することで、代理で認証情報マネージャーの呼び出しを行うことができます。これらのリクエストを処理するために、認証情報プロバイダは getOrigin() API を使用して origin を取得します。

origin を取得するには、認証情報プロバイダ アプリは、権限のある信頼できる呼び出し元のリストを androidx.credentials.provider.CallingAppInfo's getOrigin() API に渡す必要があります。この許可リストは、有効な JSON オブジェクトである必要があります。packageNamesigningInfo から取得した証明書フィンガープリントが、getOrigin() API に渡された privilegedAllowlist にあるアプリのフィンガープリントと一致する場合、origin が返されます。この origin 値を取得すると、プロバイダ アプリはこれを特権呼び出しと見なし、呼び出しアプリの署名を使って origin を計算する代わりに、AuthenticatorResponseクライアント データに origin を設定する必要があります。

origin を取得する場合は、署名リクエスト中に clientDataJSON を組み立ててハッシュ化する代わりに、CreatePublicKeyCredentialRequest() または GetPublicKeyCredentialOption() で直接提供される clientDataHash を使用します。JSON 解析の問題を回避するには、構成証明とアサーションのレスポンスに clientDataJSON のプレースホルダ値を設定します。Google パスワード マネージャーは、getOrigin() の呼び出しに、一般公開されている許可リストを使用します。認証情報プロバイダとして、このリストを使用するか、API で記述された JSON 形式で独自のリストを指定できます。使用するリストの選択はプロバイダが行います。サードパーティの認証情報プロバイダで特権アクセスを取得するには、サードパーティが提供するドキュメントをご覧ください。

デバイスでプロバイダを有効にする

ユーザーは、[デバイス設定] > [パスワードとアカウント] > [プロバイダ] > [有効にする] または [無効にする] でプロバイダを有効にする必要があります。

fun createSettingsPendingIntent(): PendingIntent