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:
- Increases account security by requiring both a password and a unique code.
- Reduces the risk of unauthorized access, even if passwords are compromised.
- Builds user trust by showing a commitment to protecting their data.
- Protects sensitive information, especially in apps that handle financial or personal details.
- Meets security standards and regulatory requirements for certain industries.
- 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:
- A user begins by entering their email, password, and phone number.
- Upon submitting, they receive a verification code on their phone.
- 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:
- First, we create an instance of the SDK and initialize the same in the
initState
of the widget. - Next, we need to create an instance
ConfirmationResult
which will be later used for verification of the code. - Later in the
registerNewUser
we are first creating the user using the email and password withfirebaseAuth?.createUserWithEmailAndPassword
which returns us to anUserCredential
object. - Using the
UserCredential
object, we are updating the user information with the role information usingfirebaseAuth?.updateUserInformation
method - Next, we are setting up the verifier as
MockApplicationVerifier
as mentioned previously and using thefirebaseAuth!.phone.signInWithPhoneNumber
to update ourconfirmationResult
- Later in the
verifyOtp
method, we are using theconfirmationResult
along with methodconfirmationResult?.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