← Back to blog

Multi-Factor Authentication with Flutter and Firebase

Guillaume Bernos

Lead Software Engineer

21st July, 2022

Need a custom tool? Invertase can help

Tired of investing in off-the-shelf software that doesn't quite fit your business? Invertase can build solutions tailored to your exact needs. Tell us what you're looking for here and we'll be in touch.

Multi-factor authentication is a feature that allows the user to add an extra layer of security. It can be a code received by SMS or saved in a special app. You’ll need to provide this code to connect when you connect to your app. If your application is manipulating sensitive data, it’s often a good idea to add Multi-factor authentication so your user’s data is more secure.

🚀 Phone multi-factor Authentication is now supported in FlutterFire! You’re going to be able to offer an extra layer of security to your users.

In this tutorial, you’ll learn how to add phone multi-factor authentication to your Flutter app, on Android, iOS, and Web!

💡 Only phone is currently supported. The support for other methods (action code) is coming soon. Moreover, Android, iOS, and Web platforms are supported.

Before you begin

  1. Go to your Firebase console, and enable the email and password provider.
  2. Ensure that your app is verifying user emails. MFA requires email verification. It’s to prevent locking out the real owner of an email from the account.

You can verify the email of your user with user.sendEmailVerification() method.

Enabling MFA

To use MFA, you first need to activate it in your Google Cloud console:

  1. Go to the Identity Platform MFA page in the Google Cloud console.
  2. In the box titled SMS-Based Multi-Factor Authentication, click Enable.
  3. Enter the phone numbers you’ll use for testing. 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 (required for Web).
  5. Click Save.

There are multiple patterns for MFA enrollment that you can check in the official documentation. Choose the one that suits your app best.

Verifying your app

For iOS, you’ll need an extra setup step for verifying your app; check the steps “Verifying your app” step on the official guide. You can either choose APN or reCaptcha verification.

Enrolling a second factor

To enroll a new secondary factor for a user:

  1. Re-authenticate the user.
  2. Ask the user for their phone number.

Note: Google stores and uses phone numbers to improve spam and abuse prevention across all Google services. Ensure you obtain appropriate consent from your users before sending their phone numbers to the Identity Platform.

1- Get the multi-factor session for the user:

final session = await user.multiFactor.getSession();

2- Verify the phone number with the multi factor session:

await FirebaseAuth.instance.verifyPhoneNumber(
  multiFactorSession: session,
  phoneNumber: phoneNumber,
  verificationCompleted: (_) {},
  verificationFailed: (_) {},
  codeSent: (String verificationId, int? resendToken) async {
   ...
  },
  codeAutoRetrievalTimeout: (_) {},
); 

3-

Get the SMS code from the user and create a new phone credential:

final credential = PhoneAuthProvider.credential(
  verificationId: verificationId,
  smsCode: smsCode,
);

4- Enroll the new factor:

await user.multiFactor.enroll(
  PhoneMultiFactorGenerator.getAssertion(
    credential,
  ),
);

Complete example:

final session = await user.multiFactor.getSession();
final auth = FirebaseAuth.instance;
await auth.verifyPhoneNumber(
  multiFactorSession: session,
  phoneNumber: phoneController.text,
  verificationCompleted: (_) {},
  verificationFailed: (_) {},
  codeSent:
      (String verificationId, int? resendToken) async {
// See `firebase_auth` example app for a method of retrieving user's sms code:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_auth/firebase_auth/example/lib/auth.dart#L591
    final smsCode = await getSmsCodeFromUser(context);

    if (smsCode != null) {
      // Create a PhoneAuthCredential with the code
      final credential = PhoneAuthProvider.credential(
        verificationId: verificationId,
        smsCode: smsCode,
      );

      try {
        await user.multiFactor.enroll(
          PhoneMultiFactorGenerator.getAssertion(
            credential,
          ),
        );
      } on FirebaseAuthException catch (e) {
        print(e.message);
      }
    }
  },
  codeAutoRetrievalTimeout: (_) {},
);

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

Sign the user in with their first factor, then catch the 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.

try {
  await _auth.signInWithEmailAndPassword(
    email: emailController.text,
    password: passwordController.text,
  );
} on FirebaseAuthMultiFactorException catch (e) {
...

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

final session = e.resolver.session;

final hint = e.resolver.hints[selectedHint];

3- Send a verification message to the user’s phone using the resolver’s session and the hint (note that you don’t need the user’s phoneNumber):

await FirebaseAuth.instance.verifyPhoneNumber(
  multiFactorSession: session,
  multiFactorInfo: hint,
  verificationCompleted: (_) {},
  verificationFailed: (_) {},
  codeSent: (String verificationId, int? resendToken) async {
   ...
  },
  codeAutoRetrievalTimeout: (_) {},
);

4- Resolve the sign in with the resolver:

final smsCode = await getSmsCodeFromUser(context);
if (smsCode != null) {
  // Create a PhoneAuthCredential with the code
  final credential = PhoneAuthProvider.credential(
    verificationId: verificationId,
    smsCode: smsCode,
  );

  try {
    await e.resolver.resolveSignIn(
      PhoneMultiFactorGenerator.getAssertion(
        credential,
      ),
    );
  } on FirebaseAuthException catch (e) {
    print(e.message);
  }
}

Complete example:

try {
  await _auth.signInWithEmailAndPassword(
    email: emailController.text,
    password: passwordController.text,
  );
} on FirebaseAuthMultiFactorException catch (e) {
  setState(() {
    error = '${e.message}';
  });
  final firstHint = e.resolver.hints.first;
  if (firstHint is! PhoneMultiFactorInfo) {
    return;
  }
  await FirebaseAuth.instance.verifyPhoneNumber(
    multiFactorSession: e.resolver.session,
    multiFactorInfo: firstHint,
    verificationCompleted: (_) {},
    verificationFailed: (_) {},
    codeSent: (String verificationId, int? resendToken) async {
// See `firebase_auth` example app for a method of retrieving user's sms code: 
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_auth/firebase_auth/example/lib/auth.dart#L591
      final smsCode = await getSmsCodeFromUser(context);

      if (smsCode != null) {
        // Create a PhoneAuthCredential with the code
        final credential = PhoneAuthProvider.credential(
          verificationId: verificationId,
          smsCode: smsCode,
        );

        try {
          await e.resolver.resolveSignIn(
            PhoneMultiFactorGenerator.getAssertion(
              credential,
            ),
          );
        } on FirebaseAuthException catch (e) {
          print(e.message);
        }
      }
    },
    codeAutoRetrievalTimeout: (_) {},
  );
} catch (e) {
  ...
} 

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

Guillaume Bernos

Lead Software Engineer

Categories

Tags