Two-factor authentication (2FA) and how to enable it in your Flutter Apps

Enhance your app’s security with Two-Factor Authentication (2FA) using Flutter and Firebase. This guide shows how to secure accounts by adding an extra verification layer, protecting user data, and building trust with a simple yet powerful 2FA setup.
Two-factor authentication (2FA) and how to enable it in your Flutter Apps
Two-factor authentication (2FA) in Flutter Apps

Today our online accounts are like vaults, holding our social interactions, professional interactions, and personal and professional data. Imagine if such a vault was protected by a single lock(a password), easily pickable by those who know how to or its weakness.

Traditional password-only systems were like this: a single point of failure that can cause unauthorized access. To truly secure our "vaults", we need multiple layers of protection, where multi-factor authentication (MFA), specifically two-factor authentication (2FA), comes in.

In this codelab, we will implement 2FA with Email and Phone using Flutter.

N.B., if you are new to Flutter, please review the official documentation to learn about it.

Why Single-Factor Authentication Falls Short?

Passwords alone are vulnerable in today's digital world. They can be leaked in data breaches, stolen, or guessed, no matter how complex it is. Someone else with access to it will gain full access to your accounts.

2FA prevents this by adding another step for verification, like a code sent to the user's phone, ensuring that when the password is compromised, an attached would still need additional information.

Introducing Two-Factor Authentication (2FA)

2FA works by pairing something we know (our password) with something we own (a verification code sent to your phone or code from the authenticator app). This significantly increases account security. For example, a bank requires you to enter both a password and a unique, time-sensitive, code sent via SMS.

Benefits of Implementing 2FA

Here’s a list of benefits of implementing 2FA:

  1. Increases account security by requiring both a password and a unique code.
  2. Reduces the risk of unauthorized access, even if passwords are compromised.
  3. Builds user trust by showing a commitment to protecting their data.
  4. Protects sensitive information, especially in apps that handle financial or personal details.
  5. Meets security standards and regulatory requirements for certain industries.
  6. Enhances brand reputation as a secure, user-focused app.

How It Works with Email and Phone Verification

In our app, we’ll implement 2FA as part of the user registration process. Here’s how it works:

  1. A user begins by entering their email, password, and phone number.
  2. Upon submitting, they receive a verification code on their phone.
  3. The user must enter this code to complete registration, ensuring that only legitimate users can sign up and access the app.

Getting Started

Prerequisites for Setting Up 2FA in Flutter

  • Ensure you have the Flutter and Dart SDK installed in your system.
  • The Dart SDK should be at the latest version (>3.2)
  • A Firebase project set up with phone and email authentication enabled

Set up the Flutter project

Download the starter project containing the prebuilt UI and minimal configuration from here.

N.B., for more details on how to set up your Flutter project, read this. I use VSCode but you can choose between other editors.

Open it in your editor, then build and run the app:

The file structure of the starter project looks like this:

The next step is to set up the Service Account Key and later the Firebase Dart Admin Auth SDK in our project.

Setting Up Firebase Service Account Keys

Step 1:
Create or Select a Firebase Project: Head to the Firebase Console to create or select a project.

Step 2:
Generate a Service Account Key:

  • Go to Project Settings > Service Accounts:
  • Click Generate New Private Key and download the JSON file:

Step 3:
Store the Key Securely: Place the key file in our Flutter project directory, ensuring it’s not exposed in version control (e.g., .gitignore), and add it in the  pubspec flutter section:

flutter:
    uses-material-design: true
    assets:
        assets/

N.B., Take note of your firebase project_id, we’ll be needing that next.

You can read more about building secure flutter apps using a service account key from here.

Adding Project ID and API Key to the Auth SDK

Before initializing the SDK, we need to update the plugin with the project ID and API key, so we have to use our Project ID from earlier and get the Web API Key from Project Settings > General as below:

Next, we need to add these details into the plugin’s main.dart as below:

import 'package:firebase_dart_admin_auth_sdk/src/firebase_auth.dart';

void main() async {
  final auth =
      FirebaseAuth(apiKey: 'YOUR_API_KEY', projectId: 'YOUR_PROJECT_ID');

  try {
    // Sign up a new user
    final newUser = await auth.createUserWithEmailAndPassword(
        'newuser@example.com', 'password123');
    print('User created: ${newUser?.user.displayName}');
    print('User created: ${newUser?.user.email}');

    // Sign in with the new user
    final userCredential = await auth.signInWithEmailAndPassword(
        'newuser@example.com', 'password123');
    print('Signed in: ${userCredential?.user.email}');
  } catch (e) {
    print('Error: $e');
  }
}

N.B., the above is the plugin's main.dart.

We are ready to initialize the Firebase Dart Admin Auth SDK and add these details to our main.dart.

Setting Up Phone Verification

In this codelab, we'll be playing with User registration with 2FA hence we'll be using Mock Application Verifier. However, a reCAPTCHA will be used as a verification mechanism to prevent automated attacks in production. This additional layer ensures that bots do not exploit the 2FA flow.

So navigate to Sign-in Methods under Authentication and add your test phone number with a mock verification code:

Adding Phone Verification During Registration

Next, whenever a new user is registered in our system, our app needs to automatically create a corresponding user in Firebase, set custom details as per requirement, and send a verification code with no intervention needed.

Here's an example:

  ...

  // 1
  FirebaseAuth? firebaseAuth;
  // 2
  ConfirmationResult? confirmationResult; // Stores verification details for OTP

  ...
  
  @override
  void initState() {
    firebaseAuth = FirebaseApp.firebaseAuth;
    super.initState();
  }

  ...

  
  Future<void> registerNewUser() async {
    try {
      // 3
      var userCredential = await firebaseAuth?.createUserWithEmailAndPassword(
          _emailController.text, _passwordController.text);
      // 4
      if (userCredential != null) {
        firebaseAuth?.updateUserInformation(userCredential.user.uid,
            userCredential.user.idToken!, {'role': _roleController.text});
        // 5
        final appVerifier = MockApplicationVerifier(); // Replace with actual recaptha verifier
        confirmationResult = await firebaseAuth!.phone.signInWithPhoneNumber(_phoneController.text, appVerifier);
      }

      setState(() {
        _status = "User created successfully. OTP sent for 2FA.";
      });
    } catch (e) {
      setState(() {
        _status = "Failed to create user: $e";
      });
    }
  }

  
  Future<void> verifyOtp() async {
    // 6
    if (confirmationResult != null) {
      try {
        await confirmationResult?.confirm(_otpController.text);
        setState(() {
          _status = "OTP verified successfully, user signed in.";
        });
      } catch (e) {
        setState(() {
          _status = "Failed to verify OTP: $e";
        });
      }
    }
  }

  ...

In the above code:

  1. First, we create an instance of the SDK and initialize the same in the initState of the widget.
  2. Next, we need to create an instance ConfirmationResult which will be later used for verification of the code.
  3. Later in the registerNewUser  we are first creating the user using the email and password with firebaseAuth?.createUserWithEmailAndPassword which returns us to an UserCredential object.
  4. Using the UserCredential object, we are updating the user information with the role information using firebaseAuth?.updateUserInformation method
  5. Next, we are setting up the verifier as MockApplicationVerifier as mentioned previously and using the firebaseAuth!.phone.signInWithPhoneNumber to update our confirmationResult
  6. Later in the verifyOtp method, we are using the confirmationResult along with method confirmationResult?.confirm to confirm the verification code

Now, we can utilize the above method in the Create User and Verify OTP button and as below:

ElevatedButton(
              onPressed: registerNewUser,
              child: const Text('Create User'),
            )

...

ElevatedButton(
              onPressed: verifyOtp,
              child: const Text('Verify OTP'),
            )

N.B., Make sure the Email/Password Sign-In Provider is enabled and use the mentioned Phone Number and verification code for testing as mentioned previously.

Verifying the User

Post creating the user, verify in your Firebase console for the new user:

Finally, a user is created using 2FA along with Firebase Dart Admin Auth SDK and before we wrap up, have a look at some edge cases, common issues and enhancements,.

Enhance 2FA, Handle Edge Cases and Common Issues

To create an optimal and smooth user experience, handle cases and tips such as:

Incorrect OTPs: Inform the user and allow retries.

Expired tokens: Set a time limit for OTPs and allow re-sending if needed.

reCAPTCHA verification and failures: Implement reCAPTHA verification and add a fallback or display a helpful error message to guide users.

Auto-fill support: On supported devices, auto-fill SMS codes to make the process seamless.

Clear messaging: Use prompts that explain each step so users understand the verification process.

Retain session data for users who verify their numbers to avoid disruptions in the sign-up flow.

Conclusion

With 2FA, you’re taking a significant step toward securing your app and building trust with users. By integrating Firebase Auth SDK, you provide a secure, scalable solution for user verification, ensuring only authorized individuals can access your app.

For the full code examples, check out my GitHub repository

About the author
Himanshu Sharma

Himanshu Sharma

Software engineer skilled in scalable data pipelines, ETL processes, and backend optimization. Passionate about Flutter, open source, and sharing insights with the developer community

Dart Code Labs

Thoughts, stories and ideas.

Dart Code Labs

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Dart Code Labs.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.