Cómo KAYAK redujo el tiempo de inicio de sesión en un 50% y mejoró la seguridad con las claves de acceso

OCT 24, 2023
Kateryna Semenova Developer Relations Engineer, Android

Introducción

KAYAK es uno de los principales motores de búsqueda de viajes del mundo y ayuda a los usuarios a encontrar las mejores ofertas en vuelos, hoteles y vehículos de alquiler. En 2023, KAYAK integró claves de acceso, un nuevo tipo de autenticación sin contraseña, en sus apps web y para Android. Como resultado, KAYAK redujo en un 50% el tiempo promedio que tardan sus usuarios en registrarse e iniciar sesión, y también registró una disminución en el número de tickets de asistencia generados.

Este caso de éxito explica la implementación que llevó a cabo KAYAK en Android con la API de Credential Manager y RxJava. Puedes usar este estudio de caso como modelo para implementar Credential Manager con la intención de mejorar la seguridad y la experiencia del usuario en tus propias apps.

Si quieres obtener un resumen rápido, mira el video complementario en YouTube.

Link to Youtube Video (visible only when JS is disabled)

Problema

Al igual que la mayoría de las empresas, KAYAK apostó en el pasado a la autenticación de usuarios por medio de contraseñas. Dichas contraseñas representan una responsabilidad para los usuarios y también para las empresas: suelen ser débiles o se reutilizan, adivinan, suplantan, filtran o piratean.

“Ofrecer autenticación con contraseñas conlleva mucho esfuerzo y riesgo para el negocio. Los atacantes intentan constantemente forzar cuentas y no todos los usuarios entienden la necesidad de contar con contraseñas seguras. Sin embargo, incluso las que sí lo son no son completamente seguras y existe la posibilidad de que se suplanten”. – Matthias Keller, director científico y vicepresidente sénior de tecnología en KAYAK

Para que la autenticación sea más segura, KAYAK envió “vínculos mágicos” por correo electrónico. Si bien es útil desde el punto de vista de la seguridad, este paso adicional introdujo un mayor grado de dificultad, ya que se pedía a los usuarios que usaran una app diferente para completar el proceso de inicio de sesión. Se necesitaban medidas adicionales para mitigar el riesgo de sufrir ataques de suplantación de identidad.

Solución

La app para Android de KAYAK ahora usa claves de acceso que brindan una experiencia de autenticación más segura, fácil de usar y más rápida. Las claves de acceso son tokens únicos y seguros que se almacenan en el dispositivo del usuario y se pueden sincronizar en varios dispositivos. Los usuarios pueden iniciar sesión en KAYAK con una clave de acceso usando simplemente el bloqueo de pantalla de su dispositivo, lo que lo hace más simple y seguro que ingresar una contraseña.

“Agregamos compatibilidad con las claves de acceso a nuestra app para Android. De esta manera, más usuarios pueden usarlas en lugar de las contraseñas. Con esta modificación, también reemplazamos nuestra antigua implementación de la API de Smartlock por el inicio de sesión con Google compatible con la API de Credential Manager. Ahora los usuarios pueden registrarse e iniciar sesión en KAYAK con claves de acceso el doble de rápido que con un vínculo de correo electrónico, lo que también mejora el porcentaje de respuesta”. – Matthias Keller, director científico y vicepresidente sénior de tecnología en KAYAK

Integración de la API de Credential Manager

Para integrar claves de acceso en Android, KAYAK utilizó la API de Credential Manager. Credential Manager es una biblioteca de Jetpack que unifica la compatibilidad de las claves de acceso a partir de Android 9 (nivel de API 28) y la compatibilidad de los métodos de inicio de sesión tradicionales, como contraseñas y autenticación federada, en una API e interfaz de usuario única.

kayak2
Figura 1: Pantallas para crear claves de acceso de Credential Manager.

Diseñar un flujo de autenticación sólido para las apps es fundamental para garantizar la seguridad y brindar una experiencia del usuario confiable. En el siguiente diagrama, se muestra cómo KAYAK integró las claves de acceso en sus flujos de registro y autenticación:

kayak3
Figura 2: Diagrama de KAYAK que muestra los flujos de registro y autenticación.

Al registrarse, los usuarios tienen la posibilidad de crear una clave de acceso. Una vez que ya se registraron, pueden iniciar sesión con su clave de acceso, iniciar sesión con Google o utilizar la contraseña. Como Credential Manager inicia la IU automáticamente, ten cuidado de no generar tiempos de espera inesperados, como las llamadas de la red. Obtén siempre un desafío de un solo uso y otra configuración de claves de acceso (como el ID de RP) al comienzo de cualquier sesión de la app.

Si bien el equipo de KAYAK invierte ahora mucho en corrutinas, en la integración inicial con la API de Credential Manager se utilizó RxJava. Se envolvieron las llamadas de Credential Manager en RxJava de la siguiente manera:

override fun createCredential(request: CreateCredentialRequest, activity: Activity): Single<CreateCredentialResponse> {
   return Single.create { emitter ->
       // Triggers credential creation flow
       credentialManager.createCredentialAsync(
           request = request,
           activity = activity,
           cancellationSignal = null,
           executor = Executors.newSingleThreadExecutor(),
           callback = object : CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
               override fun onResult(result: CreateCredentialResponse) {
                   emitter.onSuccess(result)
               }
 
               override fun onError(e: CreateCredentialException) {
                   emitter.tryOnError(e)
               }
           }
       )
   }
}

En este ejemplo, se define una función de Kotlin llamada createCredential(), que devuelve una credencial del usuario como RxJava Single del tipo CreateCredentialResponse. La función createCredential() encapsula el proceso asincrónico de registro de credenciales en un estilo de programación reactivo y utiliza la clase RxJava Single.

En relación con la implementación de Kotlin de este proceso con corrutinas, consulta la guía Inicia sesión con Credential Manager.

Flujo de registro de nuevos usuarios

Este ejemplo muestra el enfoque que KAYAK utiliza para registrar una nueva credencial. Aquí Credential Manager se encontraba envuelto en primitivas Rx.

webAuthnRetrofitService
  .getClientParams(username = /** email address **/)
  .flatMap { response ->
      // Produce a passkeys request from client params that include a one-time challenge
      CreatePublicKeyCredentialOption(/** produce JSON from response **/)
  }
  .subscribeOn(schedulers.io())
  .flatMap { request ->
      // Call the earlier defined wrapper which calls the Credential Manager UI
      // to register a new passkey credential
      credentialManagerRepository
      .createCredential(
           request = request,
           activity = activity
       )
  }
  .flatMap {
     // send credential to the authentication server
  }
  .observeOn(schedulers.main())
  .subscribe(
      { /** process successful login, update UI etc. **/ },
      { /** process error, send to logger **/ }
  )

Rx permitió que KAYAK produjera canalizaciones más complejas que pudieran involucrar múltiples interacciones con Credential Manager.

Inicio de sesión de usuario existente

KAYAK utilizó los siguientes pasos para iniciar el flujo de inicio de sesión. El proceso inicia un elemento de la IU de hoja inferior, lo que permite al usuario iniciar sesión con un ID de Google y una clave de acceso existente o una contraseña guardada.

kayak4
Figura 3: Hoja inferior para autenticación con claves de acceso.

Los desarrolladores deben seguir estos pasos al configurar un flujo de inicio de sesión:

  1. Dado que la hoja inferior se inicia automáticamente, ten cuidado de no agregar tiempos de espera inesperados en la IU, como las llamadas de red. Obtén siempre un desafío de un solo uso y otra configuración de claves de acceso (como ID de RP) al comienzo de cualquier sesión de la app.
  2. Al ofrecer el inicio de sesión de Google a través de la API de Credential Manager, tu código debe buscar inicialmente cuentas de Google que ya se hayan utilizado con la app. Para lograrlo, llama a la API con el parámetro setFilterByAuthorizedAccounts configurado en “true”.
  3. Si el resultado es una lista de credenciales disponibles, la app muestra al usuario la IU de autenticación de la hoja inferior.
  4. Si aparece NoCredentialException, no se encontraron credenciales: ni cuentas de Google, ni claves de acceso, ni contraseñas guardadas. En este punto, tu app deberá volver a llamar a la API y configurar setFilterByAuthorizedAccounts en “false” para iniciar el flujo Registrarse con Google.
  5. Procesa la credencial que muestra Credential Manager.
Single.fromSupplier<GetPublicKeyCredentialOption> {
  GetPublicKeyCredentialOption(/** Insert challenge and RP ID that was fetched earlier **/)
}
  .flatMap { response ->
      // Produce a passkeys request
      GetPublicKeyCredentialOption(response.toGetPublicKeyCredentialOptionRequest())
  }
  .subscribeOn(schedulers.io())
  .map {  publicKeyCredentialOption ->
    // Merge passkeys request together with other desired options,
    // such as Google sign-in and saved passwords.
  }
  .flatMap { request ->
    // Trigger Credential Manager system UI
    credentialManagerRepository.getCredential(
      request = request,
      activity = activity
    )
  }
  .onErrorResumeNext { throwable ->
      // When offering Google sign-in, it is recommended to first only look for Google accounts
      // that have already been used with our app. If there are no such Google accounts, no passkeys,
      // and no saved passwords, we try looking for any Google sign-in one more time.
      if (throwable is NoCredentialException) {
        return@onErrorResumeNext credentialManagerRepository.getCredential(
          request = GetCredentialRequest(/* Google ID with filterByAuthorizedOnly = false */),
          activity = activity
        )
      }
    Single.error(throwable)
  }
  .flatMapCompletable {
    // Step 1: Use Retrofit service to send the credential to the server for validation. Waiting
    // for the server is handled on a IO thread using subscribeOn(schedulers.io()).
    // Step 2: Show the result in the UI. This includes changes such as loading the profile
    // picture, updating to the personalized greeting, making member-only areas active,
    // hiding the sign-in dialog, etc. The activities of step 2 are executed on the main thread.
  }
  .observeOn(schedulers.main())
  .subscribe(
    // Handle errors, e.g. send to log ingestion service. 
    // A subset of exceptions shown to the user can also be helpful,
    // such as user setup problems. 
    // Check out more info in Troubleshoot common errors at
    // https://developer.android.com/training/sign-in/passkeys#troubleshoot
  )
“Una vez que la API de Credential Manager se implementa de forma general, es muy fácil agregar otros métodos de autenticación. Agregar Google One-Tap Sign In prácticamente no insumió ningún esfuerzo después de agregar las claves de acceso”. – Matthias Keller

Para obtener más información, sigue la guía sobre cómo Integrar la API de Credential Manager y cómo Integrar Credential Manager con el inicio de sesión con Google.

Consideraciones relacionadas con la UX

Algunas de las principales consideraciones que KAYAK debió tener en cuenta en relación con la experiencia del usuario al cambiar a claves de acceso tenían que ver con el hecho de si los usuarios debían tener la posibilidad de eliminar las claves de acceso o de crear más de una clave de acceso.

Nuestra guía de UX para claves de acceso recomienda que se tenga la opción de revocar una clave de acceso y que se asegure de que el usuario no cree claves de acceso duplicadas vinculadas al mismo nombre de usuario en el mismo administrador de contraseñas.

kayak5
Figura 4: IU de KAYAK para administrar claves de acceso.

Para evitar el registro de varias credenciales para la misma cuenta, KAYAK utilizó la propiedad excludeCredentials, que enumera las credenciales ya registradas para el usuario. En el siguiente ejemplo, se muestra cómo crear nuevas credenciales en Android sin que se generen duplicados:

fun WebAuthnClientParamsResponse.toCreateCredentialRequest(): String {
        val credentialRequest = WebAuthnCreateCredentialRequest(
            challenge = this.challenge!!.asSafeBase64,
            relayingParty = this.relayingParty!!,
            pubKeyCredParams = this.pubKeyCredParams!!,
            userEntity = WebAuthnUserEntity(
              id = this.userEntity!!.id.asSafeBase64,
                   name = this.userEntity.name,
                   displayName = this.userEntity.displayName
            ),
            authenticatorSelection = WebAuthnAuthenticatorSelection(
                  authenticatorAttachment = "platform",
                  residentKey = "preferred"
             ),
            // Setting already existing credentials here prevents
            // creating multiple passkeys on the same keychain/password manager
            excludeCredentials = this.allowedCredentials!!.map { it.copy(id = it.id.asSafeBase64) },
        )
        return GsonBuilder().disableHtmlEscaping().create().toJson(credentialRequest)
}

Y así es como KAYAK implementó la funcionalidad excludeCredentials para su implementación web.

var registrationOptions = {
   'publicKey': {
       'challenge': self.base64ToArrayBuffer(data.challenge),
       'rp': data.rp,
       'user': {
           'id': new TextEncoder().encode(data.user.id),
           'name': data.user.name,
           'displayName': data.user.displayName
       },
       'pubKeyCredParams': data.pubKeyCredParams,
       'authenticatorSelection': {
           'residentKey': 'required'
       }
   }
};
 
if (data.allowCredentials && data.allowCredentials.length > 0) {
   var excludeCredentials = [];
   for (var i = 0; i < data.allowCredentials.length; i++) {
       excludeCredentials.push({
           'id': self.base64ToArrayBuffer(data.allowCredentials[i].id),
           'type': data.allowCredentials[i].type
       });
   }
   registrationOptions.publicKey.excludeCredentials = excludeCredentials;
}
 
navigator.credentials.create(registrationOptions);

Implementación del servidor

La parte del servidor es un componente esencial de una solución de autenticación. KAYAK agregó capacidades de clave de acceso a su backend de autenticación existente con WebAuthn4J, una biblioteca Java de código abierto.

KAYAK dividió el proceso del servidor en los siguientes pasos:

  1. El cliente solicita los parámetros necesarios para crear o utilizar una clave de acceso desde el servidor. Este proceso incluye el desafío, el algoritmo de encriptación compatible, el ID del usuario de confianza y los elementos relacionados. Si el cliente ya tiene una dirección de correo electrónico de usuario, los parámetros incluirán el objeto de usuario para el registro y una lista de claves de acceso, siempre que existan.
  2. El cliente ejecuta flujos del navegador o de la app para iniciar el registro o el inicio de sesión con clave de acceso.
  3. El cliente envía la información de las credenciales recuperada al servidor. Aquí se incluyen el ID del cliente, los datos del autenticador, los datos del cliente y otros elementos relacionados. Esta información es obligatoria para crear una cuenta o verificar el inicio de sesión.

Cuando KAYAK trabajó en este proyecto, ninguno de los productos de terceros admitía claves de acceso. Sin embargo, ahora hay muchos recursos disponibles para crear un servidor de claves de acceso, incluso ejemplos de documentación y bibliotecas.

Resultados

Desde que realizó la integración de las claves de acceso, el KAYAK registró un aumento significativo de la satisfacción del usuario. Los usuarios informaron que consideran que las claves de acceso son mucho más fáciles de usar que las contraseñas, ya que no es necesario que recuerden o escriban una string larga y compleja de caracteres. KAYAK redujo un 50% el tiempo promedio que tardan sus usuarios en registrarse e iniciar sesión, observó una disminución en los tickets de asistencia que se generan en relación con las contraseñas olvidadas, y logó que su sistema sea más seguro al reducir su exposición a ataques basados en contraseñas. Gracias a estas mejoras, ​​KAYAK planea eliminar la autenticación basada en contraseñas en su app para finales de 2023.

“Las claves de acceso permiten que crear una cuenta sea una tarea muy rápida, ya que se elimina la necesidad de generar una contraseña o ir a una app separada para obtener un vínculo o código. Como beneficio adicional, la implementación de la nueva biblioteca de Credential Manager también redujo la deuda técnica en nuestra base de código al poner claves de acceso, contraseñas y el inicio de sesión de Google en una nueva IU moderna. De hecho, los usuarios pueden registrarse e iniciar sesión en KAYAK con claves de acceso el doble de rápido que con un vínculo de correo electrónico, lo que también mejora el porcentaje de respuesta”. – Matthias Keller

Conclusión

Las claves de acceso son una solución de autenticación nueva e innovadora que ofrece beneficios importantes respecto de las contraseñas tradicionales. KAYAK es un gran ejemplo de cómo una organización puede mejorar la seguridad y la usabilidad de su proceso de autenticación mediante la integración de claves de acceso. Si buscas una experiencia de autenticación más segura y fácil de usar, te recomendamos que consideres usar claves de acceso con la API de Credential Manager de Android.