Cómo migrar de FIDO2 al Administrador de credenciales

Con compatibilidad para llaves de acceso, acceso federado y proveedores de autenticación externos, Credential Manager es la API recomendada para la autenticación en Android, que proporciona un entorno seguro y conveniente que permite a los usuarios sincronizar y administrar sus credenciales. En el caso de los desarrolladores que usan credenciales FIDO2 locales, debes actualizar tu app para admitir la autenticación con llave de acceso a través de la integración con la API de Administrador de credenciales. En este documento, se describe cómo migrar tu proyecto de FIDO2 al Administrador de credenciales.

Motivos para migrar de FIDO2 al Administrador de credenciales

En la mayoría de los casos, debes migrar el proveedor de autenticación de tu app para Android al Administrador de credenciales. Estos son algunos de los motivos para migrar al Administrador de credenciales:

  • Compatibilidad con llaves de acceso: El Administrador de credenciales admite llaves de acceso, un mecanismo de autenticación nuevo y sin contraseña que es más seguro y fácil de usar que las contraseñas
  • Varios métodos de acceso: el Administrador de credenciales admite varios métodos de acceso, como contraseñas, llaves de acceso y métodos de acceso federados. Esto facilita que los usuarios se autentiquen en tu app, independientemente del método de autenticación que prefieran
  • Compatibilidad con proveedores de credenciales externos: en Android 14 y versiones posteriores, el Administrador de credenciales admite varios proveedores de credenciales externos. Esto significa que tus usuarios pueden usar sus credenciales existentes de otros proveedores para acceder a tu app
  • Experiencia del usuario coherente: el Administrador de credenciales ofrece una experiencia del usuario más coherente para la autenticación en todas las apps y los mecanismos de acceso. De esta manera, los usuarios pueden comprender y usar el flujo de autenticación de tu app con mayor facilidad

Para comenzar la migración de FIDO2 al Administrador de credenciales, sigue los pasos que se indican a continuación.

Actualiza las dependencias

  1. Actualiza el complemento de Kotlin en el archivo build.gradle de tu proyecto a 1.8.10 o una versión posterior.

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. En el archivo build.gradle de tu proyecto, actualiza las dependencias para usar el Administrador de credenciales y la autenticación de Servicios de 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. Reemplaza la inicialización de FIDO por la inicialización del Administrador de credenciales. Agrega esta declaración en la clase que usas para los métodos de creación de llaves de acceso y de acceso.

    val credMan = CredentialManager.create(context)
    

Crea llaves de acceso

Deberás crear una llave de acceso nueva, asociarla con la cuenta de un usuario y almacenar la clave pública de la llave de acceso en tu servidor antes de que el usuario pueda acceder con ella. Actualiza las llamadas a función de registro para configurar tu app con esta habilidad.

Figura 1: En esta imagen, se muestra cómo se intercambian datos entre la app y el servidor cuando se crea una llave de acceso usando el Administrador de credenciales.
  1. Para obtener los parámetros necesarios que se envían al método createCredential() durante la creación de la llave de acceso, agrega name("residentKey").value("required") como se describe en la especificación de WebAuthn a tu registerRequest() llamada al servidor.

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .method("POST", jsonRequestBody {
            name("attestation").value("none")
            name("authenticatorSelection").objectValue {
                name("residentKey").value("required")
            }
        }).build()
        // ...
    }
    
  2. Establece el tipo return para registerRequest() y todas las funciones secundarias en JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Request.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. Quita de forma segura de tu vista cualquier método que controle llamadas al selector de intents y a resultados de actividad.

  4. Dado que registerRequest() ahora muestra un JSONObject, no necesitas crear un PendingIntent. Reemplaza el intent que se muestra por una JSONObject. Actualiza las llamadas del selector de intents para llamar a createCredential() desde la API de Administrador de credenciales. Llama al método de la API de createCredential().

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
        ): CreatePublicKeyCredentialResponse? {
            val request = CreatePublicKeyCredentialRequest(requestResult.toString())
            var response: CreatePublicKeyCredentialResponse? = null
            try {
                response = credMan.createCredential(
                    request as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. Una vez que la llamada se realice de forma correcta, envía la respuesta al servidor. La solicitud y la respuesta de esta llamada son similares a la implementación de FIDO2, por lo que no se requieren cambios.

Cómo autenticar con llaves de acceso

Después de configurar la creación de llaves de acceso, puedes configurar la app para permitir que los usuarios accedan y se autentiquen con sus llaves de acceso. Para ello, actualizarás tu código de autenticación para controlar los resultados del Administrador de credenciales y, luego, implementarás una función para autenticar a través de las llaves de acceso.

Figura 2: Flujo de autenticación con llave de acceso del Administrador de credenciales.
  1. Tu llamada de solicitud de acceso al servidor para obtener la información necesaria que se enviará a la solicitud getCredential() es la misma que la de la implementación de FIDO2. No se requiere ningún cambio.
  2. Al igual que la llamada de solicitud de registro, la respuesta que se muestra está en formato 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. Quita de forma segura de tu vista cualquier método que controle llamadas al selector de intents y a resultados de actividad.

  4. Dado que signInRequest() ahora muestra un JSONObject, no necesitas crear un PendingIntent. Reemplaza el intent que se muestra por un JSONObject y llama a getCredential() desde los métodos de la API.

    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. Una vez que la llamada se realice de forma correcta, envía la respuesta al servidor para validar y autenticar al usuario. Los parámetros de solicitud y respuesta para esta llamada a la API son similares a los de la implementación de FIDO2, por lo que no se requieren cambios.

Recursos adicionales