Signing users in with multi-factor authentication

This article shows you how to add SMS multi-factor authentication to your web app.

Multi-factor authentication increases the security of your app. While attackers often compromise passwords and social accounts, gaining physical access to a user's phone is much more difficult.

Before you begin

  1. Register and sign the user in using a provider that supports multi-factor authentication. These include:

    • Email and password
    • Email link
    • Google
    • Facebook
    • Twitter
    • GitHub
    • Microsoft
    • Yahoo
    • LinkedIn
    • SAML
    • OIDC
  2. Verify the user's email. This prevents malicious actors from registering for a service with an email they don't own, and then locking out the real owner by adding a second factor.

Enabling multi-factor authentication

  1. Go to the Identity Providers page in the Cloud Console.
    Go to the Identity Providers page

  2. In the box titled Introducing Multi-Factor Authentication, click Enable. If you don't see the button, ensure you've configured a supported provider.

  3. Enter the phone numbers you'll be testing your app with. While optional, registering test phone numbers is strongly recommended to avoid throttling during development.

  4. If you haven't already authorized your app's domain, add it to the allow list by clicking Add domain on the right.

  5. Click Save.

Choosing an enrollment pattern

You can choose whether your app requires multi-factor authentication, and how and when to enroll your users. Some common patterns include:

  • Enroll the user's second factor as part of registration. Use this method if your app requires multi-factor authentication for all users.

  • Offer a skippable option to enroll a second factor during registration. Apps that want to encourage, but not require, multi-factor authentication may prefer this approach.

  • Provide the ability to add a second factor from the user's account or profile management page, instead of the sign up screen. This minimizes friction during the registration process, while still making multi-factor authentication available for security-sensitive users.

  • Require adding a second factor incrementally when the user wants to access features with increased security requirements.

Setting up the reCAPTCHA verifier

Before you can send SMS codes, you need to configure a reCAPTCHA verifier. Identity Platform uses reCAPTCHA to prevent abuse by ensuring that phone number verification requests come from one of your app's allowed domains.

You don't need to manually set up a reCAPTCHA client; the Client SDK's RecaptchaVerifier object automatically creates and initializes any necessary client keys and secrets.

Using invisible reCAPTCHA

The RecaptchaVerifier object supports invisible reCAPTCHA, which can often verify the user without requiring any interaction. To use an invisible reCAPTCHA, create a RecaptchaVerifier with the size parameter set to invisible, and specify the ID of the UI element that starts multi-factor enrollment:

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

Using the reCAPTCHA widget

To use a visible reCAPTCHA widget, create an HTML element to contain the widget, then create a RecaptchaVerifier object with the ID of the UI container. You can also optionally set callbacks that are invoked when the reCAPTCHA is solved or expires:

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

Pre-rendering the reCAPTCHA

Optionally, you can pre-render the reCAPTCHA before starting two-factor enrollment:

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

After render() resolves, you get the reCAPTCHA's widget ID, which you can use to make calls to the reCAPTCHA API:

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

Enrolling a second factor

To enroll a new secondary factor for a user:

  1. Re-authenticate the user.

  2. Ask the user enter their phone number.

  3. Initialize the reCAPTCHA verifier you configured in the previous section:

    var appVerifier = new firebase.auth.RecaptchaVerifier(container);
    
  4. Get a multi-factor session for the user:

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. Initialize a PhoneInfoOptions object with the user's phone number and the multi-factor session:

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. Send a verification message to the user's phone:

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

    While not required, it's a best practice to inform users beforehand that they will receive an SMS message, and that standard rates apply.

  7. If the request fails, reset the reCAPTCHA, then repeat the previous step so the user can try again. Note that verifyPhoneNumber() will automatically reset the reCAPTCHA when it throws an error, as reCAPTCHA tokens are one-time use only.

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    window.recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    
  8. Once the SMS code is sent, ask the user to verify the code:

    // Ask user for the verification code.
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. Initialize a MultiFactorAssertion object with the PhoneAuthCredential:

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  10. Complete the enrollment. Optionally, you can specify a display name for the second factor. This is useful for users with multiple second factors, since the phone number is masked during the authentication flow (for example, +1******1234).

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

The code below shows a complete example of enrolling a second factor:

var appVerifier = new firebase.auth.RecaptchaVerifier(container);
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, appVerifier);
})
.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);
});

Congratulations! You successfully registered a second authentication factor for a user.

Signing users in with a second factor

To sign in a user with two-factor SMS verification:

  1. Sign the user in with their first factor, then catch the auth/multi-factor-auth-required error. This error contains a resolver, hints on the enrolled second factors, and an underlying session proving the user successfully authenticated with the first factor.

    For example, if the user's first factor was an email and password:

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

    If the user's first factor is a federated provider, such as OAuth, SAML, or OIDC, catch the error after calling signInWithPopup() or signInWithRedirect().

  2. If the user has multiple secondary factors enrolled, ask them which one to use:

    // 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 {
      // Unsupported second factor.
      // Note that only phone second factors are currently supported.
    }
    
  3. Initialize the reCAPTCHA verifier you configured in the previous section:

    var appVerifier = new firebase.auth.RecaptchaVerifier(container);
    
  4. Initialize a PhoneInfoOptions object with the user's phone number and the multi-factor session. These values are contained in the resolver object passed to the auth/multi-factor-auth-required error:

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. Send a verification message to the user's phone:

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, appVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. If the request fails, reset the reCAPTCHA, then repeat the previous step so the user can try again:

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    window.recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    
  7. Once the SMS code is sent, ask the user to verify the code:

    // Ask user for the verification code.
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. Initialize a MultiFactorAssertion object with the PhoneAuthCredential:

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. Call resolver.resolverSignIn() to complete secondary authentication. You can then access the original sign-in result, which includes the standard provider-specific data and authentication credentials:

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

The code below shows a complete example of signing in a multi-factor user:

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, appVerifier)
          .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 {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

Congratulations! You successfully signed in a user using multi-factor authentication.

What's next