Cómo integrar el Administrador de credenciales en tu solución de proveedor de credenciales

El Administrador de credenciales se refiere a un conjunto de APIs que se presentaron en Android 14 y admiten varios métodos de acceso, como nombres de usuario y contraseñas, llaves de acceso y soluciones de acceso federado (como Acceder con Google). Cuando se invoca la API de Credential Manager, el sistema Android agrega las credenciales de todos los proveedores de credenciales que están instalados en el dispositivo. En este documento, se describe el conjunto de APIs que proporcionan extremos de integración para estos proveedores de credenciales.

Configuración

Antes de implementar la funcionalidad en tu proveedor de credenciales, completa los pasos de configuración que se muestran en las siguientes secciones.

Declara las dependencias

En el archivo build.gradle de tu módulo, declara una dependencia con la versión más reciente de la biblioteca del Administrador de credenciales:

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

Declara el elemento de servicio en el archivo de manifiesto

En el archivo AndroidManifest.xml de manifiesto de tu app, incluye una declaración <service> para una clase de servicio que extiende la clase CredentialProviderService de la biblioteca androidx.credentials, como se muestra en el siguiente ejemplo.

<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>

El permiso y el filtro de intents que se muestran arriba son integrales para que el flujo del Administrador de credenciales funcione como se espera. El permiso es necesario para que solo el sistema Android pueda vincularse a este servicio. El filtro de intents se usa para la visibilidad de este servicio como proveedor de credenciales que usará el Administrador de credenciales.

Declara los tipos de credenciales admitidos

En el directorio res/xml, crea un archivo nuevo llamado provider.xml. En este archivo, declara los tipos de credenciales que admite tu servicio a través de constantes definidas para cada tipo de credencial en la biblioteca. En el siguiente ejemplo, el servicio admite contraseñas tradicionales así como llaves de acceso, cuyas constantes se definen como TYPE_PASSWORD_CREDENTIAL y TYPE_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>

En niveles anteriores de API, los proveedores de credenciales integran APIs como Autofill para contraseñas y otros datos. Estos proveedores pueden usar la misma infraestructura interna para almacenar los tipos de credenciales existentes, mientras la expanden para admitir otros, incluidas las llaves de acceso.

Enfoque de dos fases en la interacción con el proveedor

El Administrador de credenciales interactúa con los proveedores de credenciales en dos fases:

  1. La primera fase es la fase de inicio/consulta, en la que el sistema se vincula a los servicios del proveedor de credenciales e invoca a los métodos onBeginGetCredentialRequest(), onBeginCreateCredentialRequest() o onClearCredentialStateRequest() con solicitudes Begin…. Los proveedores deben procesar estas solicitudes y devolver respuestas Begin…. Para ello, deben propagarlas con entradas que representan opciones visuales que se mostrarán en el selector de cuenta. Cada entrada debe tener configurado un PendingIntent.
  2. Una vez que el usuario selecciona una entrada, comienza la fase de selección y se activa el PendingIntent asociado con la entrada, lo que muestra la actividad del proveedor correspondiente. Una vez que el usuario termina de interactuar con esta actividad, el proveedor de credenciales debe establecer la respuesta en el resultado de la actividad antes de finalizarla. Esta respuesta se envía a la app cliente que invocó el Administrador de credenciales.

Controla la creación de llaves de acceso

Controla las consultas para la creación de llaves de acceso

Cuando una app cliente desea crear una llave de acceso y almacenarla con un proveedor de credenciales, llama a la API de createCredential. Para controlar esta solicitud en tu servicio de proveedor de credenciales de modo que la llave de acceso se almacene en tu almacenamiento, completa los pasos que se muestran en las siguientes secciones.

  1. Anula el método onBeginCreateCredentialRequest() en tu servicio extendido de CredentialProviderService.
  2. Para controlar la BeginCreateCredentialRequest, crea una BeginCreateCredentialResponse correspondiente y pásala a través de la devolución de llamada.
  3. Mientras creas la BeginCreateCredentialResponse, agrega el objeto CreateEntries requerido. Cada CreateEntry debe corresponder a una cuenta en la que se puede guardar la credencial y debe tener un PendingIntent configurado junto con otros metadatos obligatorios.

En el siguiente ejemplo, se muestra cómo implementar estos pasos.

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
        )
    )
}

La construcción del PendingIntent debe cumplir con lo siguiente:

  • La Activity correspondiente se debe configurar para mostrar cualquier mensaje, confirmación o selección biométrica requerida.
  • Cualquier dato requerido que el proveedor necesite cuando se invoque la actividad correspondiente debe establecerse como un objeto adicional en el intent que se usa para crear tu PendingIntent, como un accountId en el flujo de creación.
  • Tu PendingIntent debe construirse con la marca PendingIntent.FLAG_MUTABLE para que el sistema pueda agregar la solicitud final al intent adicional.
  • Tu PendingIntent no se debe construir con la marca PendingIntent.FLAG_ONE_SHOT, ya que el usuario puede seleccionar una entrada, volver atrás y volver a seleccionarla, lo que haría que PendingIntent se active dos veces.
  • Tu PendingIntent debe construirse con un código de solicitud único para que cada entrada pueda tener su PendingIntent correspondiente.

Controla la selección de entradas para las solicitudes de creación de llaves de acceso

  1. Cuando el usuario selecciona un CreateEntry propagado con anterioridad, se invoca el PendingIntent correspondiente y se crea el proveedor asociado Activity.
  2. Después de invocar el método onCreate de tu Activity, accede al intent asociado y pásalo a la clase PendingIntentHander para obtener el elemento ProviderCreateCredentialRequest.
  3. Extrae requestJson, callingAppInfo y clientDataHash de la solicitud.
  4. Extrae el accountId local del intent adicional. Esta es una implementación específica de la app de ejemplo y no es obligatoria. Este ID se puede usar para almacenar la credencial con el ID de la cuenta en particular.
  5. Valida el objeto requestJson. En el siguiente ejemplo, se usan clases de datos locales, como PublicKeyCredentialCreationOptions, para convertir el JSON de entrada en una clase estructurada según la especificación de WebAuthn. Como proveedor de credenciales, puedes reemplazarlo por tu propio analizador.
  6. Verifica el vínculo del recurso de la app que realiza la llamada si esta se origina en una app para Android nativa.
  7. Muestra un mensaje de autenticación. En el siguiente ejemplo, se usa la API de Biometric de Android.
  8. Cuando la autenticación se realice de forma correcta, genera un credentialId y un par de claves.
  9. Guarda la clave privada en tu base de datos local en callingAppInfo.packageName.
  10. Crea una respuesta JSON de la API de Web Authentication que contenga la clave pública y el credentialId. En el siguiente ejemplo, se usan clases de utilidad locales, como AuthenticatorAttestationResponse y FidoPublicKeyCredential, que ayudan a construir un JSON según las especificaciones antes mencionadas. Como proveedor de credenciales, puedes reemplazar estas clases con tus propios compiladores.
  11. Crea un objeto CreatePublicKeyCredentialResponse con el JSON que se generó antes.
  12. Configura CreatePublicKeyCredentialResponse como objeto adicional en un Intent a través de PendingIntentHander.setCreateCredentialResponse() y establece ese intent en el resultado de la actividad.
  13. Finaliza la Activity.

En el siguiente ejemplo de código, se ilustran estos pasos. Una vez que se invoque onCreate(), se deberá controlar este código en la clase de Activity.

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)}"
}

Controla las consultas para las solicitudes de creación de contraseñas

Para controlar las consultas para las solicitudes de creación de contraseñas, haz lo siguiente:

  • Dentro del método processCreateCredentialRequest() mencionado en la sección anterior, agrega otro caso dentro del bloque de interruptor para controlar las solicitudes de contraseña.
  • Mientras creas el BeginCreateCredentialResponse, agrega el objeto CreateEntries obligatorio.
  • Cada CreateEntry debe corresponder a una cuenta en la que se puede guardar la credencial y debe tener un PendingIntent configurado junto con otros metadatos.

En el siguiente ejemplo, se muestra cómo implementar estos pasos:

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)
}

Controla la selección de entradas para las solicitudes de creación de contraseñas

Cuando el usuario selecciona un objeto CreateEntry propagado, se ejecuta el PendingIntent correspondiente y se muestra la Activity asociada. Accede al intent asociado que se pasó en onCreate y pásalo a la clase PendingIntentHander para obtener el método ProviderCreateCredentialRequest.

En el siguiente ejemplo, se muestra cómo implementar este proceso. Este código debe controlarse en el método onCreate() de tu Activity.

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()

Controla el acceso de los usuarios

El acceso de los usuarios se controla siguiendo los pasos que se indican a continuación:

  • Cuando una app cliente intenta permitir el acceso de un usuario, prepara una instancia de GetCredentialRequest.
  • El framework de Android propaga esta solicitud a todos los proveedores de credenciales aplicables vinculando estos servicios.
  • Luego, el servicio del proveedor recibe una BeginGetCredentialRequest que contiene una lista de elementos BeginGetCredentialOption, cada uno de los cuales contiene parámetros que se pueden usar para recuperar credenciales coincidentes.

Para controlar esta solicitud en tu servicio de proveedor de credenciales, completa los siguientes pasos:

  1. Anula el método onBeginGetCredentialRequest() para controlar la solicitud. Ten en cuenta que, si se bloquean las credenciales, puedes establecer una AuthenticationAction en la respuesta e invocar la devolución de llamada de inmediato.

    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())
        }
    }
    

    Los proveedores que requieren el desbloqueo de las credenciales antes de mostrar cualquier credentialEntries deben configurar un intent pendiente que lleve al usuario al flujo de desbloqueo de la app:

    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. Recupera las credenciales de tu base de datos local y configúralas con CredentialEntries para que se muestren en el selector. En el caso de las llaves de acceso, puedes configurar credentialId como un extra en el intent para saber a qué credencial se asigna cuando el usuario selecciona esta entrada.

    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. Consulta las credenciales de tu base de datos, crea llaves de acceso y entradas de contraseña para que se propaguen.

    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. Una vez que consultes y propagues las credenciales, deberás controlar la fase de selección para las credenciales que selecciona el usuario, ya sea una llave de acceso o una contraseña.

Controla la selección del usuario para las llaves de acceso

  1. En el método onCreate de la actividad correspondiente, recupera el intent asociado y pásalo a PendingIntentHandler.retrieveProviderGetCredentialRequest().
  2. Extrae el GetPublicKeyCredentialOption de la solicitud recuperada anteriormente. Luego, extrae requestJson y clientDataHash de esta opción.
  3. Extrae el credentialId del intent adicional, que propagó el proveedor de credenciales cuando se configuró el PendingIntent correspondiente.
  4. Extrae la llave de acceso de la base de datos local con los parámetros de solicitud a los que se accedió más arriba.
  5. Confirma que la llave de acceso es válida con los metadatos extraídos y realiza la verificación del usuario.

    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. Para validar el usuario, muestra un mensaje biométrico (o algún otro método de aserción). El siguiente fragmento de código usa la API de Android Biometric.

  7. Una vez que la autenticación se realice correctamente, crea una respuesta JSON basada en la especificación de W3 Web Authentication Assertion. En el siguiente fragmento de código, las clases de datos auxiliares, como AuthenticatorAssertionResponse, se usan para tomar parámetros estructurados y convertirlos al formato JSON requerido. La respuesta contiene una firma digital de la clave privada de una credencial de WebAuthn. El servidor del usuario de confianza puede verificar esta firma para autenticar a un usuario antes de acceder.

  8. Crea una PublicKeyCredential con el JSON que se generó antes y establécela en un objeto GetCredentialResponse final. Establece esta respuesta final en el resultado de esta actividad.

En el siguiente ejemplo, se muestra cómo se pueden implementar estos pasos:

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)

Controla la selección del usuario para la autenticación con contraseña

  1. En tu actividad correspondiente, accede al intent que se pasó a onCreate y extrae el objeto ProviderGetCredentialRequest con PendingIntentHandler.
  2. Usa GetPasswordOption en la solicitud para recuperar las credenciales de contraseña para el nombre del paquete entrante.

    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. Una vez que lo hagas, configura la respuesta para la credencial de contraseña seleccionada.

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

Cómo controlar la selección de una entrada de acción de autenticación

Como se mencionó anteriormente, un proveedor de credenciales puede establecer una AuthenticationAction si las credenciales están bloqueadas. Si el usuario selecciona esta entrada, se invoca la actividad correspondiente a la acción de intent establecida en el PendingIntent. Luego, los proveedores de credenciales pueden mostrar un flujo de autenticación biométrica o un mecanismo similar para desbloquear las credenciales. Si se ejecuta de forma correcta, el proveedor de credenciales debe crear una BeginGetCredentialResponse, similar al modo en que se describió antes el control de acceso del usuario, ya que las credenciales ahora están desbloqueadas. Esta respuesta debe establecerse a través del método PendingIntentHandler.setBeginGetCredentialResponse() antes de que se establezca el intent preparado como resultado y finalice la actividad.

Borra las solicitudes de credenciales

Una app cliente puede solicitar que se borre cualquier estado que se mantenga para la selección de credenciales, como un proveedor de credenciales, que puede recordar la credencial seleccionada antes y mostrarla la próxima vez. Una app cliente llama a esta API y espera que se borre esa selección fija. Tu servicio de proveedor de credenciales puede controlar esta solicitud anulando el método onClearCredentialStateRequest():

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

Para permitir que los usuarios abran la configuración de tu proveedor desde la pantalla Contraseñas, llaves de acceso y autocompletar, las apps de proveedores de credenciales deben implementar el atributo del manifiesto settingsActivity credential-provider en res/xml/provider.xml. Este atributo te permite usar un intent para abrir la pantalla de configuración de tu app si un usuario hace clic en el nombre de un proveedor en la lista de servicios de Contraseñas, llaves de acceso y autocompletado. Establece el valor de este atributo en el nombre de la actividad que se iniciará desde la pantalla de configuración.

<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>
Diagrama que muestra las funciones de los botones de cambio y apertura
Figura 1: El botón Cambiar abre el diálogo de selección existente, lo que le permite al usuario seleccionar su proveedor de credenciales preferido. El botón Open inicia la actividad de configuración definida en el cambio de manifiesto y abre una página de configuración específica para ese proveedor.

Intents de configuración

Abrir configuración: El intent android.settings.CREDENTIAL_PROVIDER abre una pantalla de configuración en la que el usuario puede seleccionar sus proveedores de credenciales preferidos y adicionales.

Pantalla de configuración de Contraseñas, llaves de acceso y Autocompletar
Figura 2: Pantalla de configuración de Contraseñas, llaves de acceso y Autocompletar

Servicio de credenciales preferido: El intent ACTION_REQUEST_SET_AUTOFILL_SERVICE redirecciona al usuario a la pantalla de selección del proveedor preferido. El proveedor seleccionado en esta pantalla se convierte en el proveedor de credenciales y Autocompletar preferido.

Diagrama que muestra las funciones de los botones de cambio y apertura
Figura 3: Pantalla de configuración de Servicio preferido para contraseñas, llaves de acceso y autocompletar.

Obtén una lista de apps con privilegios permitidas

Las apps con privilegios, como los navegadores web, realizan llamadas al Administrador de credenciales en nombre de otros usuarios de confianza si se configura el parámetro origin en GetCredentialRequest() y CreatePublicKeyCredentialRequest() del Administrador de credenciales. Para procesar estas solicitudes, el proveedor de credenciales recupera el origin usando la API de getOrigin().

Para recuperar el origin, la app del proveedor de credenciales debe pasar una lista de emisores con privilegios y de confianza a la API de androidx.credentials.provider.CallingAppInfo's getOrigin(). Esta lista de entidades permitidas debe ser un objeto JSON válido. Se muestra el origin si el packageName y las huellas digitales del certificado obtenidas de signingInfo coinciden con las de una app que se encuentra en la privilegedAllowlist que se pasa a la API de getOrigin(). Después de obtener el valor de origin, la app del proveedor debe considerarla una llamada con privilegios y establecer este origin en los datos del cliente en la AuthenticatorResponse, en lugar de calcular el origin con la firma de la app que realiza la llamada.

Si recuperas un origin, usa el clientDataHash que se proporciona directamente en CreatePublicKeyCredentialRequest() o GetPublicKeyCredentialOption(), en lugar de organizar clientDataJSON y generarle un hash durante la solicitud de firma. Para evitar problemas de análisis de JSON, configura un valor de marcador de posición para clientDataJSON en la respuesta de certificación y aserción. El Administrador de contraseñas de Google usa una lista de entidades permitidas disponible para las llamadas a getOrigin(). Como proveedor de credenciales, puedes usar esta lista o proporcionar la tuya en el formato JSON que describe la API. Depende del proveedor seleccionar qué lista se usará. Para obtener acceso privilegiado con proveedores de credenciales externos, consulta la documentación proporcionada por el tercero.

Habilita proveedores en un dispositivo

Los usuarios deben habilitar el proveedor a través de las opciones configuración del dispositivo > cuentas y contraseñas > tu proveedor > habilitar o inhabilitar.

fun createSettingsPendingIntent(): PendingIntent