El Administrador de credenciales es una API de Jetpack que admite varios métodos de acceso, como nombres de usuario y contraseñas, llaves de acceso y soluciones de acceso federado (como Acceder con Google) en una sola API, lo que simplifica la integración para los desarrolladores.
Además, para los usuarios, el Administrador de credenciales unifica la interfaz de acceso en todos los métodos de autenticación, lo que permite que los usuarios accedan a las apps de manera más clara y sencilla, independientemente del método que elijan.
Acerca de las llaves de acceso
Las llaves de acceso son el reemplazo más seguro y sencillo de las contraseñas. Con las llaves de acceso, los usuarios pueden acceder a apps y sitios web mediante un sensor biométrico (como una huella dactilar o el reconocimiento facial), un PIN o un patrón. Esto proporciona una experiencia de acceso fluida, lo que permite que los usuarios no tengan que recordar nombres de usuario o contraseñas.
Las llaves de acceso dependen de WebAuthn (Web Authentication), un estándar desarrollado de forma conjunta por la FIDO Alliance y el World Wide Web Consortium (W3C). WebAuthn usa la criptografía de clave pública para autenticar al usuario. El sitio web o la app a los que accede el usuario pueden ver y almacenar la clave pública, pero nunca la privada. Esta se mantiene en secreto y segura. Debido a que la clave es única y está vinculada al sitio web o la app, las llaves de acceso no son vulnerables a la suplantación de identidad (phishing), lo que aumenta la seguridad.
El Administrador de credenciales permite que los usuarios creen llaves de acceso y las almacenen en el Administrador de contraseñas de Google.
Requisitos previos
Para usar el Administrador de credenciales, completa los pasos que se indican en esta sección.
Cómo usar una versión reciente de la plataforma
El Administrador de credenciales es compatible con Android 4.4 (nivel de API 19) y versiones posteriores.
Agrega dependencias a tu app
Agrega las siguientes dependencias al archivo build.gradle
del módulo de tu app:
Groovy
dependencies { implementation "androidx.credentials:credentials:1.0.0-alpha02" // optional - needed for credentials support from play services, for devices running // Android 13 and below. implementation "androidx.credentials:credentials-play-services-auth:1.0.0-alpha02" }
Kotlin
dependencies { implementation("androidx.credentials:credentials:1.0.0-alpha02") // optional - needed for credentials support from play services, for devices running // Android 13 and below. implementation("androidx.credentials:credentials-play-services-auth:1.0.0-alpha02") }
Cómo agregar compatibilidad con Vínculos de recursos digitales
Si deseas admitir las llaves de acceso en tu app para Android, asóciala con un sitio web que le pertenezca a la app. Para declarar esta asociación, completa los siguientes pasos:
Crea un archivo JSON de Vínculos de recursos digitales. Por ejemplo, para declarar que el sitio web
https://signin.example.com
y una app para Android con el nombre de paquetecom.example
pueden compartir credenciales de acceso, crea un archivo llamadoassetlinks.json
con el siguiente contenido:[{ "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://signin.example.com" } }, { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "com.example", "sha256_cert_fingerprints": [ SHA_HEX_VALUE ] } }]
El campo
relation
es un array de una o más strings que describen la relación que se declara. Para declarar que las apps y los sitios comparten credenciales de acceso, especifica la stringdelegate_permission/common.get_login_creds
.El campo
target
es un objeto que especifica el recurso al que se aplica la declaración. Los siguientes campos identifican un sitio web:namespace
web
site
La URL del sitio web, en el formato
https://domain[:optional_port]
; por ejemplo,https://www.example.com
.domain debe estar completamente calificado y se debe omitir optional_port cuando se usa el puerto 443 para HTTPS.
Un destino
site
solo puede ser un dominio raíz: no puedes limitar una asociación de app a un subdirectorio específico. No incluyas una ruta en la URL, como una barra final.No se considera que los subdominios coincidan, es decir, si especificas domain como
www.example.com
, el dominiowww.counter.example.com
no estará asociado a tu app.En los siguientes campos, se identifica una app para Android:
namespace
android_app
package_name
El nombre de paquete declarado en el manifiesto de la app. Por ejemplo: com.example.android
.sha256_cert_fingerprints
Las huellas dactilares SHA256 del certificado de firma de tu app. Aloja el archivo JSON de Vínculos de recursos digitales en la siguiente ubicación del dominio de acceso:
https://domain[:optional_port]/.well-known/assetlinks.json
Por ejemplo, si el dominio de acceso es
signin.example.com
, aloja el archivo JSON enhttps://signin.example.com/.well-known/assetlinks.json
.El tipo de MIME del archivo de Vínculos de recursos digitales debe ser JSON. Asegúrate de que el servidor envíe un encabezado
Content-Type: application/json
en la respuesta.Asegúrate de que tu host permita que Google recupere tu archivo de Vínculos de recursos digitales. Si tienes un archivo
robots.txt
, este debe permitir que el agente de Googlebot recupere/.well-known/assetlinks.json
. La mayoría de los sitios pueden permitir que cualquier agente automatizado recupere archivos en la ruta de acceso/.well-known/
para que otros servicios puedan acceder a los metadatos en esos archivos:User-agent: * Allow: /.well-known/
Declara la asociación en la app para Android:
Agrega un recurso de cadenas
asset_statements
al archivostrings.xml
. La stringasset_statements
es un objeto JSON que especifica los archivosassetlinks.json
que se cargarán. Debes escapar cualquier apóstrofo y comillas que uses en la string. Por ejemplo:<string name="asset_statements" translatable="false"> [{ \"include\": \"https://signin.example.com/.well-known/assetlinks.json\" }] </string>
> GET /.well-known/assetlinks.json HTTP/1.1 > User-Agent: curl/7.35.0 > Host: signin.example.com < HTTP/1.1 200 OK < Content-Type: application/json
Cómo configurar el Administrador de credenciales
Para configurar e inicializar un objeto CredentialManager
, agrega una lógica similar a la siguiente:
Kotlin
// Use your app or activity context to instantiate a client instance of // CredentialManager. val credentialManager = CredentialManager.create(context)
Java
// Use your app or activity context to instantiate a client instance of // CredentialManager. CredentialManager credentialManager = CredentialManager.create(context)
Dale acceso a tu usuario
Para recuperar todas las opciones de llaves de acceso y contraseñas asociadas con la cuenta del usuario, completa estos pasos:
Inicializa las opciones de autenticación con contraseñas y llaves de acceso:
Kotlin
// Retrieves the user's saved password for your app from their // password provider. val getPasswordOption = GetPasswordOption() // Get passkeys from the user's public key credential provider. val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( requestJson = requestJson, preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials )
Java
// Retrieves the user's saved password for your app from their // password provider. GetPasswordOption getPasswordOption = new GetPasswordOption(); // Get passkeys from the user's public key credential provider. GetPublicKeyCredentialOption getPublicKeyCredentialOption = new GetPublicKeyCredentialOption(requestJson, preferImmediatelyAvailableCredentials);
Compila la solicitud de acceso con las opciones recuperadas del paso anterior:
Kotlin
val getCredRequest = GetCredentialRequest( listOf(getPasswordOption, getPublicKeyCredentialOption) )
Java
GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder() .addCredentialOption(getPasswordOption) .addCredentialOption(getPublicKeyCredentialOption) .build();
Inicia el flujo de acceso:
Kotlin
coroutineScope.launch { try { val result = credentialManager.getCredential( request = getCredRequest, activity = activity, ) handleSignIn(result) } catch (e : GetCredentialException) { handleFailure(e) } } fun handleSignIn(result: GetCredentialResponse) { // Handle the successfully returned credential. val credential = result.credential when (credential) { is PublicKeyCredential -> { responseJson = credential.authenticationResponseJson fidoAuthenticateWithServer(responseJson) } is PasswordCredential -> { val username = credential.id val password = credential.password passwordAuthenticateWithServer(username, password) } else -> { // Catch any unrecognized credential type here. Log.e(TAG, "Unexpected type of credential") } } }
Java
credentialManager.getCredentialAsync( getCredRequest, activity, cancellationSignal, requireContext().getMainExecutor(), new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() { @Override public void onResult(GetCredentialResponse result) { // Handle the successfully returned credential. Credential credential = result.getCredential(); if (credential instanceof PublicKeyCredential) { String responseJson = ((PublicKeyCredential) credential) .getAuthenticationResponseJson(); fidoAuthenticateToServer(responseJson); } else if (credential instanceof PasswordCredential) { Log.d(TAG, "Got PasswordCredential"); String id = ((PasswordCredential) credential).getId(); String password = ((PasswordCredential) credential) .getPassword(); firebaseSignInWithPassword(id, password); } else { Log.e( TAG, "Unexpected type of credential: " + credential.getClass().getName()); } } @Override public void onError(GetCredentialException e) { Log.e(TAG, "Sign in failed with exception", e); } } );
En el siguiente fragmento, se muestra un ejemplo sobre la forma de dar formato a la solicitud JSON cuando obtienes una llave de acceso:
{
"challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
"allowCredentials": [],
"timeout": 1800000,
"userVerification": "required",
"rpId": "credential-manager-app-test.glitch.me"
}
En el siguiente fragmento de código, se muestra un ejemplo de respuesta JSON después de obtener una credencial de clave pública:
{
"id": "KEDetxZcUfinhVi6Za5nZQ",
"type": "public-key",
"rawId": "KEDetxZcUfinhVi6Za5nZQ",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
"authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
"signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
"userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
}
}
Flujos de registro
Puedes registrar un usuario para la autenticación mediante una llave de acceso o una contraseña.
Crea una llave de acceso
Para darles a los usuarios la opción de inscribir una llave de acceso y usarla para la reautenticación, registra una credencial de usuario mediante un objeto CreatePublicKeyCredentialRequest
:
Kotlin
fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) { val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( // Contains the request in JSON format. Uses the standard WebAuthn // web JSON spec. requestJson = requestJson, // Defines whether you prefer to use only immediately available credentials, // not hybrid credentials, to fulfill this request. This value is false // by default. preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, ) // Execute CreateCredentialRequest asynchronously to register credentials // for a user account. Handle success and failure cases with the result and // exceptions, respectively. coroutineScope.launch { try { val result = credentialManager.createCredential( request = createPublicKeyCredentialRequest, activity = activity, ) handlePasskeyRegistrationResult(result) } catch (e : CreateCredentialException){ handleFailure(e) } } fun handleFailure(e: CreateCredentialException) { when (e) { is CreatePublicKeyCredentialDomException -> { // Handle the passkey DOM errors thrown according to the // WebAuthn spec. handlePasskeyError(e.domError) } is CreateCredentialCancellationException -> { // The user intentionally canceled the operation and chose not // to register the credential. } is CreateCredentialInterruptedException -> { // Retry-able error. Consider retrying the call. } is CreateCredentialProviderConfigurationException -> { // Your app is missing the provider configuration dependency. // Most likely, you're missing the // "credentials-play-services-auth" module. } is CreateCredentialUnknownException -> ... is CreateCustomCredentialException -> { // You have encountered an error from a 3rd-party SDK. If you // make the API call with a request object that's a subclass of // CreateCustomCredentialRequest using a 3rd-party SDK, then you // should check for any custom exception type constants within // that SDK to match with e.type. Otherwise, drop or log the // exception. } else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") } } }
Java
public void createPasskey(String requestJson, boolean preferImmediatelyAvailableCredentials) { CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest = // `requestJson` contains the request in JSON format. Uses the standard // WebAuthn web JSON spec. // `preferImmediatelyAvailableCredentials` defines whether you prefer // to only use immediately available credentials, not hybrid credentials, // to fulfill this request. This value is false by default. new CreatePublicKeyCredentialRequest( requestJson, preferImmediatelyAvailableCredentials); // Execute CreateCredentialRequest asynchronously to register credentials // for a user account. Handle success and failure cases with the result and // exceptions, respectively. credentialManager.createCredentialAsync( createPublicKeyCredentialRequest, requireActivity(), cancellationSignal, requireContext().getMainExecutor(), new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> { @Override public void onResult(CreateCredentialResponse result) { handleSuccessfulCreatePasskeyResult(result); } @Override public void onError(CreateCredentialException e) { if (e instanceof CreatePublicKeyCredentialDomException) { // Handle the passkey DOM errors thrown according to the // WebAuthn spec. handlePasskeyError(((CreatePublicKeyCredentialDomException)e).getDomError()); } else if (e instanceof CreateCredentialCancellationException) { // The user intentionally canceled the operation and chose not // to register the credential. } else if (e instanceof CreateCredentialInterruptedException) { // Retry-able error. Consider retrying the call. } else if (e instanceof CreateCredentialProviderConfigurationException) { // Your app is missing the provider configuration dependency. // Most likely, you're missing the // "credentials-play-services-auth" module. } else if (e instanceof CreateCredentialUnknownException) { } else if (e instanceof CreateCustomCredentialException) { // You have encountered an error from a 3rd-party SDK. If // you make the API call with a request object that's a // subclass of // CreateCustomCredentialRequest using a 3rd-party SDK, // then you should check for any custom exception type // constants within that SDK to match with e.type. // Otherwise, drop or log the exception. } else { Log.w(TAG, "Unexpected exception type " + e.getClass().getName()); } } } ); }
Agrega formato a la solicitud JSON
Después de crear una llave de acceso, debes asociarla con la cuenta de un usuario y almacenar la clave pública de la llave de acceso en tu servidor. En el siguiente fragmento, se muestra un ejemplo sobre la manera de dar formato a la solicitud JSON cuando creas una llave de acceso.
En esta entrada de blog sobre cómo llevar la autenticación sin problemas a tus apps, se muestra cómo dar formato a tu solicitud JSON cuando creas llaves de acceso y cuando te autenticas con llaves de acceso. También se explica por qué las contraseñas no son una solución de autenticación eficaz, cómo aprovechar las credenciales biométricas existentes, cómo asociar tu app a un sitio web de tu propiedad, cómo crear llaves de acceso y cómo realizar la autenticación con llaves de acceso.
{
"challenge": "nhkQXfE59Jb97VyyNJkvDiXucMEvltduvcrDmGrODHY",
"rp": {
"name": "CredMan App Test",
"id": "credential-manager-app-test.glitch.me"
},
"user": {
"id": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0",
"name": "helloandroid@gmail.com",
"displayName": "helloandroid@gmail.com"
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
],
"timeout": 1800000,
"attestation": "none",
"excludeCredentials": [],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"requireResidentKey": true,
"residentKey": "required",
"userVerification": "required"
}
}
Controla la respuesta JSON
En el siguiente fragmento de código, se muestra un ejemplo de respuesta JSON para crear una credencial de clave pública. Obtén más información para procesar la credencial de clave pública que se muestra.
{
"id": "KEDetxZcUfinhVi6Za5nZQ",
"type": "public-key",
"rawId": "KEDetxZcUfinhVi6Za5nZQ",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"
}
}
Guarda la contraseña del usuario
Si el usuario proporciona un nombre de usuario y una contraseña para un flujo de autenticación en tu app, puedes registrar una credencial de usuario que pueda usarse para autenticarlo. Para hacerlo, crea un objeto CreatePasswordRequest
:
Kotlin
fun registerPassword(username: String, password: String) { // Initialize a CreatePasswordRequest object. val createPasswordRequest = CreatePasswordRequest(id = username, password = password) // Create credentials and handle result. coroutineScope.launch { try { val result = credentialManager.createCredential(createPasswordRequest) handleRegisterPasswordResult(result) } catch (e: CreateCredentialException) { handleFailure(e) } } }
Java
void registerPassword(String username, String password) { // Initialize a CreatePasswordRequest object. CreatePasswordRequest createPasswordRequest = new CreatePasswordRequest(username, password); // Register the username and password. credentialManager.createCredentialAsync( createPasswordRequest, requireActivity(), cancellationSignal, requireContext().getMainExecutor(), new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> { @Override public void onResult(CreateCredentialResponse result) { handleResult(result); } @Override public void onError(CreateCredentialException e) { handleFailure(e) } } ); }
Cómo solucionar los problemas comunes
En la siguiente tabla, se muestran varios códigos de error frecuentes y sus descripciones, y se proporciona información sobre las causas:
Código de error y descripción | Causa |
---|---|
On Begin Sign In Failure: 16: Se bloqueó temporalmente el llamador porque se cancelaron demasiados mensajes de acceso. | Si se produce este período de enfriamiento de 24 horas durante el desarrollo, puedes restablecerlo borrando el almacenamiento de la app de los Servicios de Google Play. Como alternativa, para activar o desactivar este enfriamiento en un dispositivo o emulador de prueba, ve a la app de Teléfono e ingresa el siguiente código: |
On Begin Sign In Failure: 8: Error interno desconocido. |
|
CreatePublicKeyCredentialDomException: La solicitud entrante no se puede validar. | El ID de paquete de la app no está registrado en tu servidor. Valida esto en la integración del servidor. |