Agregar autenticación de varios factores a una aplicación web

En este documento, se muestra cómo agregar la autenticación de varios factores mediante SMS a tu app web.

La autenticación de varios factores aumenta la seguridad de tu app. Si bien los atacantes a menudo hackean las contraseñas y las cuentas de redes sociales, interceptar un mensaje de texto es más difícil.

Antes de comenzar

  1. Habilita al menos un proveedor que admita la autenticación de varios factores. Todos los proveedores admiten la MFA, excepto la autenticación por teléfono, la autenticación anónima y Game Center de Apple.

  2. Asegúrate de que en tu app se verifiquen los correos electrónicos de los usuarios. La MFA requiere la verificación por correo electrónico. Esto evita que los actores maliciosos se registren en un servicio con un correo electrónico que no les pertenece y, luego, bloqueen al propietario real agregando un segundo factor.

Usa la funcionalidad multiusuario

Si habilitas la autenticación de varios factores para usarla en un entorno de multiusuario, asegúrate de completar los siguientes pasos (además del resto de las instrucciones en este documento):

  1. En la consola de Google Cloud, selecciona el usuario con el que deseas trabajar.

  2. En tu código, configura el campo tenantId en la instancia Auth como el ID de tu usuario. Por ejemplo:

    Web versión 9

    import { getAuth } from "firebase/auth";
    
    const auth = getAuth(app);
    auth.tenantId = "myTenantId1";
    

    Web versión 8

    firebase.auth().tenantId = 'myTenantId1';
    

Habilita la autenticación de varios factores

  1. Ve a la página MFA de Identity Platform en la consola de Google Cloud.
    Ir a la página MFA

  2. En el cuadro llamado Autenticación de varios factores basada en SMS, haz clic en Habilitar.

  3. Ingresa los números de teléfono con los que probarás tu app. Si bien es opcional, se recomienda registrar los números de teléfono de prueba para evitar los límites durante el desarrollo.

  4. Si aún no autorizaste el dominio de tu app, haz clic en Agregar dominio a la derecha para agregarlo a la lista de permisos.

  5. Haz clic en Guardar.

Elige un patrón de inscripción

Puedes elegir si tu app requerirá una autenticación de varios factores, además de cómo y cuándo inscribir a tus usuarios. Entre los patrones comunes, se incluyen los siguientes:

  • Inscribir el segundo factor del usuario como parte del registro. Usa este método si tu app requiere la autenticación de varios factores para todos los usuarios.

  • Ofrecer una opción que se puede omitir para inscribir un segundo factor durante el registro. Es posible que las apps que quieran fomentar el proceso de autenticación de varios factores, pero que no lo requieran, prefieran este enfoque.

  • Proporcionar la capacidad de agregar un segundo factor desde la página de administración de la cuenta o el perfil del usuario, en lugar de la pantalla de registro. Esto minimiza la fricción durante el proceso de registro y, a la vez, permite que la autenticación de varios factores esté disponible para los usuarios sensibles a la seguridad.

  • Requiere agregar un segundo factor de manera incremental cuando el usuario quiera acceder a las funciones con requisitos de seguridad mayores.

Configura el verificador de reCAPTCHA

Antes de poder enviar códigos de SMS, debes configurar un verificador de reCAPTCHA. Identity Platform usa reCAPTCHA para impedir que las solicitudes de verificación de números de teléfono provengan de uno de los dominios permitidos de tu app con el fin de evitar el abuso.

No necesitas configurar manualmente un cliente de reCAPTCHA. El objeto RecaptchaVerifier del SDK del cliente crea e inicializa automáticamente las claves y los secretos necesarios del cliente.

Usa reCAPTCHA invisible

El objeto RecaptchaVerifier admite un reCAPTCHA invisible, que a menudo puede verificar al usuario sin necesidad de interacción. Para usar un reCAPTCHA invisible, crea un RecaptchaVerifier con el parámetro size configurado como invisible y especifica el ID del elemento de la IU que inicia la inscripción de varios factores:

Web versión 9

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier("sign-in-button", {
    "size": "invisible",
    "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
    }
}, auth);

Web versión 8

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
  // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
  onSolvedRecaptcha();
}
});

Usa el widget de reCAPTCHA

Para usar el widget de un reCAPTCHA visible, crea un elemento HTML que contenga el widget y, luego, crea un objeto RecaptchaVerifier con el ID del contenedor de la IU. De forma opcional, también puedes configurar devoluciones de llamada que se invoquen cuando el reCAPTCHA se resuelva o venza:

Web versión 9

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier(
    "recaptcha-container",

    // Optional reCAPTCHA parameters.
    {
      "size": "normal",
      "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
      },
      "expired-callback": function() {
        // Response expired. Ask user to solve reCAPTCHA again.
        // ...
      }
    }, auth
);

Web versión 8

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
  'recaptcha-container',
  // Optional reCAPTCHA parameters.
  {
    'size': 'normal',
    'callback': function(response) {
      // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
      // ...
      onSolvedRecaptcha();
    },
    'expired-callback': function() {
      // Response expired. Ask user to solve reCAPTCHA again.
      // ...
    }
  });

Renderiza previamente el reCAPTCHA

De manera opcional, puedes renderizar previamente el reCAPTCHA antes de iniciar la inscripción de dos factores:

Web versión 9

recaptchaVerifier.render()
    .then(function (widgetId) {
        window.recaptchaWidgetId = widgetId;
    });

Web versión 8

recaptchaVerifier.render()
  .then(function(widgetId) {
    window.recaptchaWidgetId = widgetId;
  });

Cuando render() se resuelve, obtienes el ID del widget de reCAPTCHA, que puedes usar para hacer llamadas a la API de reCAPTCHA de la siguiente manera:

var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);

RecaptchaVerifier abstrae esta lógica del método verify, por lo que no necesitas controlar la variable grecaptcha directamente.

Inscribe un segundo factor

Si quieres inscribir un nuevo factor secundario para un usuario, haz lo siguiente:

  1. Vuelve a autenticar al usuario.

  2. Pídele al usuario que ingrese su número de teléfono.

  3. Inicializa el verificador de reCAPTCHA como se indicó en la sección anterior. Omite este paso si ya se configuró una instancia de RecaptchaVerifier:

    Web versión 9

    import { RecaptchaVerifier } from "firebase/auth";
    
    const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web versión 8

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. Obtén una sesión de varios factores para el usuario:

    Web versión 9

    import { multiFactor } from "firebase/auth";
    
    multiFactor(user).getSession().then(function (multiFactorSession) {
        // ...
    });
    

    Web versión 8

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. Inicializa un objeto PhoneInfoOptions con el número de teléfono del usuario y la sesión de varios factores:

    Web versión 9

    // Specify the phone number and pass the MFA session.
    const phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    

    Web versión 8

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. Envía un mensaje de verificación al teléfono del usuario:

    Web versión 9

    import { PhoneAuthProvider } from "firebase/auth";
    
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed to complete enrollment.
        });
    

    Web versión 8

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for enrollment completion.
      })
    

    Si bien no es obligatorio, se recomienda informar con anticipación a los usuarios que recibirán un mensaje SMS y que se aplicarán las tarifas estándar.

  7. Si la solicitud falla, restablece el reCAPTCHA y, luego, repite el paso anterior para que el usuario pueda volver a intentarlo. Ten en cuenta que verifyPhoneNumber() restablecerá automáticamente el reCAPTCHA cuando arroje un error, ya que los tokens de reCAPTCHA son solo para un único uso.

    Web versión 9

    recaptchaVerifier.clear();
    

    Web versión 8

    recaptchaVerifier.clear();
    
  8. Después de que se envíe el código SMS, pídele al usuario que lo verifique:

    Web versión 9

    // Ask user for the verification code. Then:
    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web versión 8

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. Inicializa un objeto MultiFactorAssertion con la PhoneAuthCredential:

    Web versión 9

    import { PhoneMultiFactorGenerator } from "firebase/auth";
    
    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web versión 8

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  10. Completa la inscripción. De manera opcional, puedes especificar un nombre visible para el segundo factor. Esto resulta útil para usuarios con múltiples segundos factores, ya que el número de teléfono se enmascara durante el flujo de autenticación (por ejemplo, +1******1234).

    Web versión 9

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
    

    Web versión 8

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
    

En el siguiente código, se muestra un ejemplo completo de la inscripción de un segundo factor:

Web versión 9

import {
    multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
    RecaptchaVerifier
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
multiFactor(user).getSession()
    .then(function (multiFactorSession) {
        // Specify the phone number and pass the MFA session.
        const phoneInfoOptions = {
            phoneNumber: phoneNumber,
            session: multiFactorSession
        };

        const phoneAuthProvider = new PhoneAuthProvider(auth);

        // Send SMS verification code.
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
    }).then(function (verificationId) {
        // Ask user for the verification code. Then:
        const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
        const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);

        // Complete enrollment.
        return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
    });

Web versión 8

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
user.multiFactor.getSession().then(function(multiFactorSession) {
  // Specify the phone number and pass the MFA session.
  var phoneInfoOptions = {
    phoneNumber: phoneNumber,
    session: multiFactorSession
  };
  var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
  // Send SMS verification code.
  return phoneAuthProvider.verifyPhoneNumber(
      phoneInfoOptions, recaptchaVerifier);
})
.then(function(verificationId) {
  // Ask user for the verification code.
  var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
  var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
  // Complete enrollment.
  return user.multiFactor.enroll(multiFactorAssertion, mfaDisplayName);
});

¡Felicitaciones! Registraste correctamente un segundo factor de autenticación para un usuario.

Permite que los usuarios accedan con un segundo factor

Para que un usuario acceda con la verificación mediante SMS de dos factores, haz lo siguiente:

  1. Permite que el usuario acceda con el primer factor y, luego, detecta el error auth/multi-factor-auth-required. Este contiene un agente de resolución, sugerencias sobre los segundos factores inscritos y una sesión subyacente, si el usuario se autenticó correctamente con el primer factor.

    Por ejemplo, si el primer factor del usuario era un correo electrónico y una contraseña, haz lo siguiente:

    Web versión 9

    import { getAuth, getMultiFactorResolver} from "firebase/auth";
    
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email, password)
        .then(function (userCredential) {
            // User successfully signed in and is not enrolled with a second factor.
        })
        .catch(function (error) {
            if (error.code == 'auth/multi-factor-auth-required') {
                // The user is a multi-factor user. Second factor challenge is required.
                resolver = getMultiFactorResolver(auth, error);
                // ...
            } else if (error.code == 'auth/wrong-password') {
                // Handle other errors such as wrong password.
            }
    });
    

    Web versión 8

    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(function(userCredential) {
        // User successfully signed in and is not enrolled with a second factor.
      })
      .catch(function(error) {
        if (error.code == 'auth/multi-factor-auth-required') {
          // The user is a multi-factor user. Second factor challenge is required.
          resolver = error.resolver;
          // ...
        } else if (error.code == 'auth/wrong-password') {
          // Handle other errors such as wrong password.
        } ...
      });
    

    Si el primer factor del usuario es un proveedor federado, como OAuth, OIDC o SAML, detecta el error después de llamar a signInWithPopup() o signInWithRedirect().

  2. Si el usuario tiene múltiples factores secundarios inscritos, pregúntale cuál usará:

    Web versión 9

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    
    if (resolver.hints[selectedIndex].factorId ===
        PhoneMultiFactorGenerator.FACTOR_ID) {
        // User selected a phone second factor.
        // ...
    } else if (resolver.hints[selectedIndex].factorId ===
               TotpMultiFactorGenerator.FACTOR_ID) {
        // User selected a TOTP second factor.
        // ...
    } else {
        // Unsupported second factor.
    }
    

    Web versión 8

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    if (resolver.hints[selectedIndex].factorId === firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
      // User selected a phone second factor.
      // ...
    } else if (resolver.hints[selectedIndex].factorId === firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. Inicializa el verificador de reCAPTCHA como se indicó en la sección anterior. Omite este paso si ya se configuró una instancia de RecaptchaVerifier:

    Web versión 9

    import { RecaptchaVerifier } from "firebase/auth";
    
    recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web versión 8

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. Inicializa un objeto PhoneInfoOptions con el número de teléfono del usuario y la sesión de varios factores. Estos valores se encuentran en el objeto resolver que se pasa al error auth/multi-factor-auth-required:

    Web versión 9

    const phoneInfoOptions = {
        multiFactorHint: resolver.hints[selectedIndex],
        session: resolver.session
    };
    

    Web versión 8

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. Envía un mensaje de verificación al teléfono del usuario:

    Web versión 9

    // Send SMS verification code.
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed for sign-in completion.
        });
    

    Web versión 8

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. Si la solicitud falla, restablece el reCAPTCHA y, luego, repite el paso anterior para que el usuario pueda volver a intentarlo:

    Web versión 9

    recaptchaVerifier.clear();
    

    Web versión 8

    recaptchaVerifier.clear();
    
  7. Después de que se envíe el código SMS, pídele al usuario que lo verifique:

    Web versión 9

    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web versión 8

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. Inicializa un objeto MultiFactorAssertion con la PhoneAuthCredential:

    Web versión 9

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web versión 8

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. Llama a resolver.resolveSignIn() para completar la autenticación secundaria. Luego, puedes acceder al resultado del acceso original, en el que se incluyen los datos específicos del proveedor y las credenciales de autenticación estándar:

    Web versión 9

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
        .then(function (userCredential) {
            // userCredential will also contain the user, additionalUserInfo, optional
            // credential (null for email/password) associated with the first factor sign-in.
    
            // For example, if the user signed in with Google as a first factor,
            // userCredential.additionalUserInfo will contain data related to Google
            // provider that the user signed in with.
            // - user.credential contains the Google OAuth credential.
            // - user.credential.accessToken contains the Google OAuth access token.
            // - user.credential.idToken contains the Google OAuth ID token.
        });
    

    Web versión 8

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
      .then(function(userCredential) {
        // userCredential will also contain the user, additionalUserInfo, optional
        // credential (null for email/password) associated with the first factor sign-in.
        // For example, if the user signed in with Google as a first factor,
        // userCredential.additionalUserInfo will contain data related to Google provider that
        // the user signed in with.
        // user.credential contains the Google OAuth credential.
        // user.credential.accessToken contains the Google OAuth access token.
        // user.credential.idToken contains the Google OAuth ID token.
      });
    

En el siguiente código, se muestra un ejemplo completo de acceso de un usuario de varios factores:

Web versión 9

import {
    getAuth,
    getMultiFactorResolver,
    PhoneAuthProvider,
    PhoneMultiFactorGenerator,
    RecaptchaVerifier,
    signInWithEmailAndPassword
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function (userCredential) {
        // User is not enrolled with a second factor and is successfully
        // signed in.
        // ...
    })
    .catch(function (error) {
        if (error.code == 'auth/multi-factor-auth-required') {
            const resolver = getMultiFactorResolver(auth, error);
            // Ask user which second factor to use.
            if (resolver.hints[selectedIndex].factorId ===
                PhoneMultiFactorGenerator.FACTOR_ID) {
                const phoneInfoOptions = {
                    multiFactorHint: resolver.hints[selectedIndex],
                    session: resolver.session
                };
                const phoneAuthProvider = new PhoneAuthProvider(auth);
                // Send SMS verification code
                return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
                    .then(function (verificationId) {
                        // Ask user for the SMS verification code. Then:
                        const cred = PhoneAuthProvider.credential(
                            verificationId, verificationCode);
                        const multiFactorAssertion =
                            PhoneMultiFactorGenerator.assertion(cred);
                        // Complete sign-in.
                        return resolver.resolveSignIn(multiFactorAssertion)
                    })
                    .then(function (userCredential) {
                        // User successfully signed in with the second factor phone number.
                    });
            } else if (resolver.hints[selectedIndex].factorId ===
                       TotpMultiFactorGenerator.FACTOR_ID) {
                // Handle TOTP MFA.
                // ...
            } else {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

Web versión 8

var resolver;
firebase.auth().signInWithEmailAndPassword(email, password)
  .then(function(userCredential) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      resolver = error.resolver;
      // Ask user which second factor to use.
      if (resolver.hints[selectedIndex].factorId ===
          firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
        var phoneInfoOptions = {
          multiFactorHint: resolver.hints[selectedIndex],
          session: resolver.session
        };
        var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
        // Send SMS verification code
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
          .then(function(verificationId) {
            // Ask user for the SMS verification code.
            var cred = firebase.auth.PhoneAuthProvider.credential(
                verificationId, verificationCode);
            var multiFactorAssertion =
                firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
            // Complete sign-in.
            return resolver.resolveSignIn(multiFactorAssertion)
          })
          .then(function(userCredential) {
            // User successfully signed in with the second factor phone number.
          });
      } else if (resolver.hints[selectedIndex].factorId ===
        firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
        // Handle TOTP MFA.
        // ...
      } else {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

¡Felicitaciones! Permitiste acceder a un usuario correctamente mediante la autenticación de varios factores.

¿Qué sigue?