image

01 Jan 2026

9K

35K

Flutter & Firebase Auth: Implementing SMS OTP Login

SMS OTP (One-Time Password) login provides a robust and widely accessible authentication method for mobile applications. By leveraging Flutter for cross-platform development and Firebase Authentication for its powerful backend services, developers can seamlessly integrate this feature, offering users a secure and convenient login experience. This article will guide you through the process of setting up and implementing SMS OTP login using Flutter and Firebase Auth.

Prerequisites

Before you begin, ensure you have:

  • Flutter SDK installed and configured.
  • A Google account.
  • Basic understanding of Flutter development.

Firebase Project Setup

The first step is to set up your Firebase project and enable Phone Authentication.

1. Create a Firebase Project

Navigate to the Firebase console and create a new project. Follow the on-screen instructions.

2. Enable Phone Authentication

Once your project is created:

  1. In the Firebase console, go to "Authentication" from the left-hand menu.
  2. Click on the "Sign-in method" tab.
  3. Find "Phone" in the list of sign-in providers and enable it.

3. Add Your Flutter App to Firebase

You'll need to add your Android and/or iOS app to the Firebase project.

For Android:

  1. Click the Android icon on your Firebase project overview page.
  2. Register your app by providing the package name (found in android/app/src/main/AndroidManifest.xml under the package attribute).
  3. Crucially, add the SHA-1 fingerprint. To get your SHA-1 fingerprint, open your Android project in a terminal and run:
    
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    
    Then copy the SHA1 fingerprint and paste it into the Firebase console. This is essential for Phone Authentication to work correctly.
  4. Download the google-services.json file and place it in your Flutter project's android/app/ directory.
  5. Update your android/build.gradle and android/app/build.gradle files to include Firebase plugins (instructions provided during Firebase setup or consult Firebase docs).

For iOS:

  1. Click the iOS icon on your Firebase project overview page.
  2. Register your app by providing the iOS bundle ID (found in Xcode under your project settings, General tab).
  3. Download the GoogleService-Info.plist file and place it in your Xcode project's Runner folder. Ensure it's added to your targets.
  4. Enable Push Notifications and Background Modes -> Remote notifications in Xcode for your app.

Flutter Project Setup

Now, let's configure your Flutter project.

1. Create a Flutter Project


flutter create sms_otp_login
cd sms_otp_login

2. Add Dependencies

Open your pubspec.yaml file and add the following dependencies:


dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.X.X # Use the latest version, e.g., ^2.24.2
  firebase_auth: ^4.X.X # Use the latest version, e.g., ^4.15.2

Run flutter pub get to fetch the new packages.

3. Initialize Firebase in Flutter

You need to initialize Firebase when your app starts. Add the following to your main.dart:


import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart'; // Import for FirebaseAuth later

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SMS OTP Login',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AuthScreen(), // We'll create this screen next
    );
  }
}

Implementing SMS OTP Login

Now, let's build the UI and logic for sending and verifying SMS OTPs. Create a new file, auth_screen.dart, or integrate directly into your main.dart. For demonstration, we'll use a new AuthScreen. Make sure to update your main.dart to import auth_screen.dart if you put it in a separate file.

1. User Interface (auth_screen.dart)

The UI will consist of an input field for the phone number, a button to send the OTP, an input field for the OTP, and a button to verify it.


import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

class AuthScreen extends StatefulWidget {
  const AuthScreen({super.key});

  @override
  State createState() => _AuthScreenState();
}

class _AuthScreenState extends State {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  TextEditingController _phoneController = TextEditingController();
  TextEditingController _otpController = TextEditingController();

  String _verificationId = '';
  bool _codeSent = false;
  String? _errorMessage;

  @override
  void dispose() {
    _phoneController.dispose();
    _otpController.dispose();
    super.dispose();
  }

  Future _verifyPhoneNumber() async {
    setState(() {
      _errorMessage = null;
    });

    await _auth.verifyPhoneNumber(
      phoneNumber: '+${_phoneController.text}', // Ensure full international format
      verificationCompleted: (PhoneAuthCredential credential) async {
        // Auto-retrieve a verification code
        await _auth.signInWithCredential(credential);
        _navigateToHomeScreen();
      },
      verificationFailed: (FirebaseAuthException e) {
        setState(() {
          _errorMessage = e.message;
        });
        print('Verification Failed: ${e.message}');
      },
      codeSent: (String verificationId, int? resendToken) {
        setState(() {
          _verificationId = verificationId;
          _codeSent = true;
        });
        print('Code Sent to phone. Verification ID: $verificationId');
      },
      codeAutoRetrievalTimeout: (String verificationId) {
        setState(() {
          _verificationId = verificationId;
          _codeSent = true; // Still consider code sent even if auto-retrieval times out
        });
        print('Auto retrieval timeout. Verification ID: $verificationId');
      },
      timeout: const Duration(seconds: 60),
    );
  }

  Future _signInWithPhoneNumber() async {
    setState(() {
      _errorMessage = null;
    });

    try {
      PhoneAuthCredential credential = PhoneAuthProvider.credential(
        verificationId: _verificationId,
        smsCode: _otpController.text,
      );
      await _auth.signInWithCredential(credential);
      _navigateToHomeScreen();
    } on FirebaseAuthException catch (e) {
      setState(() {
        _errorMessage = e.message;
      });
      print('Sign In Failed: ${e.message}');
    }
  }

  void _navigateToHomeScreen() {
    // Replace with your actual home screen navigation
    Navigator.of(context).pushReplacement(
      MaterialPageRoute(builder: (context) => const HomeScreen()),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Phone Auth')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (!_codeSent) ...[
              TextField(
                controller: _phoneController,
                keyboardType: TextInputType.phone,
                decoration: const InputDecoration(
                  labelText: 'Phone Number (e.g., 6281234567890)',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: _verifyPhoneNumber,
                child: const Text('Send OTP'),
              ),
            ],
            if (_codeSent) ...[
              TextField(
                controller: _otpController,
                keyboardType: TextInputType.number,
                decoration: const InputDecoration(
                  labelText: 'Enter OTP',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: _signInWithPhoneNumber,
                child: const Text('Verify OTP'),
              ),
            ],
            if (_errorMessage != null)
              Padding(
                padding: const EdgeInsets.only(top: 20),
                child: Text(
                  _errorMessage!,
                  style: const TextStyle(color: Colors.red),
                  textAlign: TextAlign.center,
                ),
              ),
          ],
        ),
      ),
    );
  }
}

// Dummy Home Screen for navigation
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Welcome')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You are logged in!', style: TextStyle(fontSize: 24)),
            ElevatedButton(
              onPressed: () async {
                await FirebaseAuth.instance.signOut();
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(builder: (context) => const AuthScreen()),
                );
              },
              child: const Text('Logout'),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation of the Code:

  • _phoneController and _otpController manage the input fields.
  • _verificationId stores the ID received from Firebase after sending the OTP, which is crucial for verifying the OTP later.
  • _codeSent is a boolean flag to toggle between showing the phone number input and the OTP input.
  • _verifyPhoneNumber(): This is the core function for initiating the OTP process.
    • phoneNumber: Must be in the E.164 format (e.g., +1234567890).
    • verificationCompleted: Called when Firebase automatically verifies the phone number without requiring an OTP (e.g., on Android when SMS is auto-retrieved).
    • verificationFailed: Handles errors during the verification process.
    • codeSent: This is where you get the verificationId. Store it and update the UI to prompt for the OTP.
    • codeAutoRetrievalTimeout: If the SMS isn't auto-retrieved within the timeout, this is called. The verificationId is still provided, and you'd proceed to manually enter the OTP.
  • _signInWithPhoneNumber(): Once the user enters the OTP, this function creates a PhoneAuthCredential using the stored _verificationId and the entered OTP, then attempts to sign in.
  • _navigateToHomeScreen(): A placeholder for navigating to your app's main content after successful login.

Important Considerations and Error Handling

  • Phone Number Format: Always ensure the phone number is in the E.164 format (e.g., +<country_code><phone_number>).
  • SHA-1 Fingerprint (Android): Without the correct SHA-1 fingerprint registered in Firebase, Phone Authentication will fail on Android with an invalid-credential or app-not-authorized error.
  • Quota Exceeded: Firebase imposes quotas on SMS messages. Repeated testing might lead to temporary blocks.
  • Invalid OTP: Handle the invalid-verification-code error when the user enters a wrong OTP.
  • UI Feedback: Provide clear loading indicators and error messages to the user.
  • Testing with Real Devices: Phone authentication requires a physical device (or a correctly configured emulator/simulator) for receiving SMS messages. Firebase Test Phone Numbers and SMS Codes can be configured in the Firebase console for easier testing without incurring SMS costs or hitting quotas.

Conclusion

You have successfully implemented SMS OTP login in your Flutter application using Firebase Authentication. This powerful combination provides a secure, user-friendly, and cross-platform authentication solution. By following these steps, you can enhance the security and accessibility of your mobile applications, offering a seamless experience to your users. Remember to consider edge cases and provide robust error handling for a production-ready application.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is