Agrega la autenticación de varios factores a una app web

Si actualizaste a Firebase Authentication con Identity Platform, puedes 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 GCP, 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:

    API modular web

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

    API con espacio de nombres web

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

Habilita la autenticación de varios factores

  1. Abre la página Autenticación > Método de acceso de Firebase console.

  2. En la sección Opciones avanzadas, habilita la Autenticación de varios factores mediante SMS.

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

  3. Si aún no autorizas el dominio de tu app, agrégalo a la lista de entidades permitidas en la página Autenticación > Configuración de Firebase console.

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. Firebase usa reCAPTCHA para asegurarse de 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:

API modular web

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

API con espacio de nombres web

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:

API modular web

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

API con espacio de nombres web

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:

API modular web

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

API con espacio de nombres web

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:

    API modular web

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

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

    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:

    API modular web

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

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

    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.

    API modular web

    recaptchaVerifier.clear();
    

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

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

API modular web

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

API con espacio de nombres web

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:

    API modular web

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

    API con espacio de nombres web

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

    API modular web

    // 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.
    }
    

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

    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:

    API modular web

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

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

    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:

    API modular web

    recaptchaVerifier.clear();
    

    API con espacio de nombres web

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

    API modular web

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

    API con espacio de nombres web

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

    API modular web

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    API con espacio de nombres web

    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:

    API modular web

    // 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.
        });
    

    API con espacio de nombres web

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

API modular web

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

API con espacio de nombres web

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.

Próximos pasos