将凭据管理器与您的凭据提供程序解决方案集成

凭据管理器指的是 Android 14 中引入的一组 API,这些 API 支持多种登录方法,例如用户名/密码、密钥通行证和联合登录解决方案(例如“使用 Google 登录”)。当调用凭据管理器 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(如密码和其他数据的自动填充)集成。这些提供程序可以使用相同的内部基础架构来存储现有的凭据类型,同时扩展它以支持其他类型,包括密钥通行证。

提供程序交互的两阶段方法

凭据管理器分两个阶段与凭据提供程序交互

  1. 第一阶段是开始/查询阶段,在此阶段,系统绑定到凭据提供程序服务并调用onBeginGetCredentialRequest()onBeginCreateCredentialRequest()onClearCredentialStateRequest()方法,并使用Begin…请求。提供程序必须处理这些请求并使用Begin…响应进行响应,并在其中填充代表帐户选择器上显示的可视选项的条目。每个条目都必须设置一个PendingIntent
  2. 一旦用户选择了一个条目,选择阶段就会开始,并且与该条目关联的PendingIntent会被触发,从而调出相应的提供程序活动。一旦用户完成与该活动的交互,凭据提供程序必须在结束活动之前将响应设置为活动的返回值。然后,此响应将发送到调用凭据管理器的客户端应用。

处理密钥创建

处理密钥创建查询

当客户端应用希望创建密钥并将其与凭据提供程序一起存储时,它们会调用createCredential API。为了在您的凭据提供程序服务中处理此请求,以便密钥实际存储在您的存储中,请完成以下部分中所示的步骤。

  1. 覆盖从CredentialProviderService扩展的服务中的onBeginCreateCredentialRequest()方法。
  2. 通过构造相应的BeginCreateCredentialResponse并将其通过回调传递来处理BeginCreateCredentialRequest
  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构建应遵循以下规则

  • 相应的Activity应设置为显示任何所需的生物识别提示、确认或选择。
  • 在用于创建PendingIntent的Intent中,应将提供程序在调用相应活动时所需的任何必要数据设置为额外的信息,例如创建流程中的accountId
  • 您的PendingIntent必须使用标志PendingIntent.FLAG_MUTABLE构建,以便系统可以将最终请求附加到Intent的额外信息中。
  • 您的PendingIntent不能使用标志PendingIntent.FLAG_ONE_SHOT构建,因为用户可能会选择一个条目,然后返回并重新选择它,这会导致PendingIntent触发两次。
  • 您的PendingIntent必须使用唯一的请求代码构建,以便每个条目都可以有其自己的相应PendingIntent

处理密钥创建请求的条目选择

  1. 当用户选择一个先前填充的CreateEntry时,相应的PendingIntent会被调用,并且会创建关联的提供程序Activity
  2. 在Activity的onCreate方法被调用后,访问关联的Intent并将其传递到PendingIntentHander类以获取ProviderCreateCredentialRequest
  3. 从请求中提取requestJsoncallingAppInfoclientDataHash
  4. 从Intent的额外信息中提取本地accountId。这是一个示例应用特定的实现,不是必需的。此帐户ID可用于针对此特定帐户ID存储此凭据。
  5. 验证requestJson。以下示例使用诸如PublicKeyCredentialCreationOptions之类的本地数据类,根据WebAuthn规范将输入JSON转换为结构化类。作为凭据提供程序,您可以用您自己的解析器替换它。
  6. 如果调用源自原生Android应用,请检查调用应用的asset-link
  7. 显示身份验证提示。以下示例使用Android生物识别API。
  8. 身份验证成功后,生成credentialId密钥对
  9. 私钥存储在您本地数据库中,对应于callingAppInfo.packageName
  10. 构造一个Web Authentication API JSON响应,其中包含公钥credentialId。以下示例使用诸如AuthenticatorAttestationResponseFidoPublicKeyCredential之类的本地实用程序类,帮助根据前面提到的规范构建JSON。作为凭据提供程序,您可以用您自己的构建器替换这些类。
  11. 使用上面生成的JSON构造一个CreatePublicKeyCredentialResponse
  12. 通过PendingIntentHander.setCreateCredentialResponse()CreatePublicKeyCredentialResponse作为额外的信息设置到Intent上,并将该Intent设置为Activity的结果。
  13. 结束Activity。

以下代码示例说明了这些步骤。此代码需要在您的Activity类中处理,一旦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()方法内部,在switch块中添加另一个case用于处理密码请求。
  • 在构造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会执行并调出关联的Activity。访问在onCreate中传递的关联Intent,并将其传递到PendingIntentHander类以获取ProviderCreateCredentialRequest方法。

以下示例说明了如何实现此过程。此代码需要在您的Activity的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框架通过绑定到这些服务将此请求传播到所有适用的凭据提供程序。
  • 然后,提供程序服务接收一个BeginGetCredentialRequest,其中包含一个BeginGetCredentialOption列表,每个选项都包含可用于检索匹配凭据的参数。

为了在您的凭据提供程序服务中处理此请求,请完成以下步骤

  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之前需要解锁凭据的提供程序必须设置一个挂起的Intent,该Intent将用户导航到应用的解锁流程

    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作为Intent的额外信息设置,以便在用户选择此条目时知道它映射到哪个凭据。

    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. 在相应Activity的onCreate方法中,检索关联的Intent,并将其传递到PendingIntentHandler.retrieveProviderGetCredentialRequest()
  2. 从上面检索到的请求中提取GetPublicKeyCredentialOption。随后,从此选项中提取requestJsonclientDataHash
  3. 从Intent的额外信息中提取credentialId,该信息是在设置相应的PendingIntent时由凭据提供程序填充的。
  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生物识别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.xml中实现credential-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>
Diagram showing the change and open button functions
图 1:更改”按钮打开现有的选择对话框,允许用户选择其首选的凭据提供程序。“打开”按钮启动清单更改中定义的设置活动,并打开专门为此提供程序提供的设置页面。

设置意图

打开设置android.settings.CREDENTIAL_PROVIDER意图会调出一个设置屏幕,用户可以在其中选择其首选和额外的凭据提供程序。

The Passwords, passkeys, and autofill settings screen
图 2:密码、密钥和自动填充设置屏幕。

首选凭据服务ACTION_REQUEST_SET_AUTOFILL_SERVICE意图会将您的用户重定向到首选提供程序选择屏幕。此屏幕上的选定提供程序将成为首选凭据和自动填充提供程序。

Diagram showing the change and open button functions
图 3:密码、密钥和自动填充设置屏幕的首选服务。

获取特权应用的白名单

诸如 Web 浏览器之类的特权应用代表其他依赖方进行凭据管理器调用,方法是在凭据管理器的GetCredentialRequest()CreatePublicKeyCredentialRequest()方法中设置origin参数。为了处理这些请求,凭据提供程序使用getOrigin() API检索origin

为了检索origin,凭据提供程序应用需要将特权和受信任的调用方列表传递到androidx.credentials.provider.CallingAppInfo's getOrigin() API。此白名单必须是一个有效的 JSON 对象。如果从signingInfo获得的packageName和证书指纹与在传递到getOrigin() API的privilegedAllowlist中找到的应用的证书指纹匹配,则会返回origin。获取origin值后,提供程序应用应将其视为特权调用,并在客户端数据中设置此origin(在AuthenticatorResponse中),而不是使用调用应用的签名计算origin

如果检索到origin,请使用CreatePublicKeyCredentialRequest()GetPublicKeyCredentialOption()中直接提供的clientDataHash,而不是在签名请求期间组装和散列clientDataJSON。为了避免 JSON 解析问题,请在证明和断言响应中为clientDataJSON设置一个占位符值。Google 密码管理器使用一个公开可用的白名单来调用getOrigin()。作为凭据提供程序,您可以使用此列表或以 API 描述的 JSON 格式提供您自己的列表。提供程序可以自行选择使用哪个列表。要获得第三方凭据提供程序的特权访问,请参阅第三方提供的文档。

在设备上启用提供程序

用户必须通过设备设置 > 密码和帐户 > 您的提供程序 > 启用或禁用来启用提供程序。

fun createSettingsPendingIntent(): PendingIntent