hero

How to implement offline authentication with Firebase auth in Flutter projects

Maintaining a seamless and uninterrupted user experience is crucial while implementing Firebase auth in Flutter projects, particularly in scenarios where internet connectivity might be sporadic or entirely unavailable. While implementing Firebase authentication in Flutter can be intricate, incorporating offline capabilities adds an extra layer of complexity. This article delves into the techniques and strategies to implement offline authentication seamlessly in Flutter projects using Firebase Authentication.

Vinod Pal

Vinod Pal

Why do we need offline authentication in apps?

  1. User Experience: Seamless access to features in areas with poor or no internet connectivity.
  2. Accessibility: Users can authenticate and use certain features offline when needed.
  3. Security: Enhances security by allowing offline access to specific features or data.
  4. Reduced Dependency on Servers: Provides a fallback mechanism during server outages.
  5. Compliance and Regulations: Meets regulatory requirements for accessibility and security.

    Implementing offline authentication in flutter

Let's dive into the steps to implement offline authentication in the Flutter project. In this comprehensive guide, we'll cover the authentication in Flutter using Firebase, and implement authentication offline.

Prerequisites

  1. Flutter is installed on your local machine.
  2. Android Studio or Android SDK is installed on your local machine.
  3. A text editor installed, e.g., VsCode.
  4. Knowledge of authentication using Firebase Get-Started-Flutter-Firebase-Auth
  5. Basic knowledge of Flutter app development

    Set up a flutter project and app

Let’s create a new Flutter project, run the following command to create a new Flutter app

flutter create myflutterapp


The command provided will create a fresh Flutter project within a directory containing your project's name, which, in this case, is myflutterapp.

cd myflutterapp


Open this in your Favorite IDE, I am using vscode. Once your project is set up the project structure will look something like this

Firebase auth in Flutter 00


This is what a default Flutter project structure looks like.

Setup the firebase for authentication

We will be using Firebase to authenticate our Flutter app. Before setting up our project with the Firebase we need have install Firebase CLI. Using this CLI we can set up a firebase for our project.
Before using Firebase CLI we will have to log in to Firebase and run the below command to log into the Firebase.

firebase login

To validate if the login was done successfully, run the below command

firebase projects:list

This will list all the projects that you have available in your account. Next, we have to install FlutterFire CLI, and run the below command to install the FlutterFire CLI

dart pub global activate flutterfire_cli

Now, we will configure the FlutterFire to our project. Run this command to configure the FlutterFire to our newly created app

flutterfire configure

Here, select your project from the list of projects and proceed. Once this is done, it will create a new file in our project with the name firebase_options.dart.

Initialize firebase in the project

Before initializing the Firebase for our project, we will have to install a few Firebase-related packages. Here are installing firebase_core, firebase_auth, and firebase_ui_auth

flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add firebase_ui_auth


Once these packages are installed, go to main.dart file and replace the main method with the below code

>>lib/main.dart


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

WidgetsFlutterBinding.ensureInitialized()

This line prepares Flutter's binding for Firebase.

Firebase.initializeApp()

This line initializes Firebase with default platform options. This is essential for setting up Firebase services before the app starts. Also, make sure to import the firebase-related packages

import 'package:firebase_core/firebase_core.dart';
import 'package:myflutterapp/firebase_options.dart';

Now we have set up our project with Firebase.

Checkpoint #1

Let us now check if we are on the right track, run the application using the below command

flutter run

After running this command, you should see the application running on port 8000. If any port-related errors occur, ensure that port 8000 is not already in use.

Firebase auth in Flutter 01


Here I am choosing Chrome to test the application, you can choose whichever you are comfortable with. After choosing your device, it will launch the application

Firebase auth in Flutter 02


The application will look something like this in the browser

Firebase auth in Flutter 03


Now we have our basic project setup ready, it is time to add the relevant files to add authentication to it

Add authentication to the application

Before implementing authentication, we will refactor our code to organize it correctly Delete all the boilerplate code from main.dart file and the below code it

lib/main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:myflutterapp/app.dart';
import 'package:myflutterapp/firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

Now, let's create a new file in the lib folder name it as app.dart, and add the below code to it


lib/app.dart

import 'package:flutter/material.dart';
import 'package:myflutterapp/login_screen.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Flutter App',
      home: LoginScreen(),
    );
  }
}

Here we are defining a simple app structure named "My Flutter App" with a starting screen set to login screen. The MyApp class is a stateless widget representing the overall app. It’s time to add a login screen to our project, create a new file name login_screen.dart, and add the below code to it

lib/login_screen.dart

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:myflutterapp/authenticate.dart';
import 'package:myflutterapp/home.dart';

class LoginScreen extends StatelessWidget {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();

  LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Flutter App'),
      ),
      body: Container(
        decoration: const BoxDecoration(
          color: Colors.blueGrey,
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextField(
                controller: emailController,
                decoration: InputDecoration(
                  labelText: 'Email',
                  filled: true,
                  fillColor: Colors.white,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                controller: passwordController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  filled: true,
                  fillColor: Colors.white,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                ),
                obscureText: true,
              ),
              const SizedBox(height: 32),
              ElevatedButton(
                onPressed: () async {
                  String email = emailController.text;
                  String password = passwordController.text;
                  try {
                    AuthResponse? user =
                        await Authentication.authenticate(email, password);

                    if (user != null && user.isAuthenticated) {
                      Navigator.pushReplacement(
                        context,
                        MaterialPageRoute(
                          builder: (context) => const MyHomePage(
                            title: "My Flutter App",
                          ),
                        ),
                      );
                    } else {
                      SnackBar snackBara = SnackBar(
                        content: Text(
                          user!.message,
                          style: const TextStyle(fontSize: 16),
                        ),
                        backgroundColor: Colors.red,
                      );

                      ScaffoldMessenger.of(context).showSnackBar(snackBara);
                    }
                  } on FirebaseAuthException catch (e) {
                    print("Error: ${e.message}");
                    const snackBar = SnackBar(
                      content: Text(
                        'Authentication failed. Please check your credentials.',
                        style: TextStyle(fontSize: 16),
                      ),
                      backgroundColor: Colors.red,
                    );

                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                },
                style: ElevatedButton.styleFrom(
                  primary: Colors.green,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                  padding:
                      const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
                ),
                child: const Text(
                  'Login',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Here we have created a simple login screen with Firebase authentication. Utilizing the firebase_auth and flutter/material.dart dependencies, we have added text input fields for email and password, styled with rounded corners. A login button triggers Firebase authentication, providing user feedback through SnackBars. Successful logins navigate to the home page, while authentication failures prompt error messages. Now, let’s create a file for authentication, name it authenticate.dart, and add the below code to it

lib/authenticate.dart

import 'package:firebase_auth/firebase_auth.dart';

class Authentication {
  static Future<AuthResponse?> authenticate(
      String email, String password) async {
    // Online authentication with Firebase
    try {
      UserCredential userCredential = await FirebaseAuth.instance
          .signInWithEmailAndPassword(email: email, password: password);
      return AuthResponse(
          username: email,
          userDetails: userCredential.user,
          isAuthenticated: true,
          message: "Login Success");
    } on FirebaseAuthException catch (e) {
      // Handle authentication errors
      print("Error: ${e.message}");
      return AuthResponse(
          username: email, isAuthenticated: false, message: e.message!);
    }
  }
}

class AuthResponse {
  final String username;
  final User? userDetails;
  final bool isAuthenticated;
  final String message;

  AuthResponse(
      {required this.username,
      this.userDetails,
      required this.isAuthenticated,
      required this.message});
}

Here, we created an authentication class in Dart, utilizing the Firebase Authentication package. The Authentication class encapsulated authentication functionality, featuring a static method named authenticate. This method handled the user sign-in process by taking email and password parameters.
Within the authenticate method, Firebase's signInWithEmailAndPassword is added to sign in the user. Upon successful sign-in, an AuthResponse object is generated. This object included details like the username (email), user specifics, authentication status (set to true), and a success message.
In the event of authentication errors, caught through a FirebaseAuthException, the error message is printed to the console for debugging purposes. Subsequently, an AuthResponse object is created, incorporating the username, authentication status set to false, and the error message.
The AuthResponse class holds information regarding the authentication result, encompassing the username, user details, authentication status, and a message. This class is returned by the authenticate method to furnish details about the concluded authentication process. Finally, let's create a home page where we will redirect our users after a successful login. Create a new file and name it home.dart and add the below code to it

lib/home.dart

import 'package:flutter/material.dart';
import 'package:myflutterapp/login_screen.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
        actions: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ElevatedButton(
              onPressed: () async {
                Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(builder: (context) => LoginScreen()),
                );
              },
              style: ElevatedButton.styleFrom(
                primary: Colors.red, // Change the color as needed
                onPrimary: Colors.white,
              ),
              child: const Text('Log Out'),
            ),
          ),
        ],
      ),
      body: Center(
        child: Container(
          padding: const EdgeInsets.all(16.0),
          child: const Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              Text(
                'Welcome to the Cool App!',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 20),
              Text(
                'Explore the amazing features and enjoy your experience.',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 16, color: Colors.grey),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Here, we defined a stateful widget called MyHomePage. The widget takes a title as a parameter and creates a state associated with it. The home page includes an app bar with a title and a "Log Out" button in the app bar's actions. Clicking the "Log Out" button triggers navigation to the login screen using Navigator.pushReplacement.
The body of the home page consists of a centered container containing a column of text widgets. The text widgets provide a welcome message and a brief description of the app's features. The app bar has a custom background color obtained from the theme.

Checkpoint #2


With the authentication integrated into our Flutter app, it's time to conduct a test. Run the application, and you should now see the login screen, as seen below.

Firebase auth in Flutter 04


Enter the credentials that you have registered in the Firebase.
If you do not have users created then you can follow these steps to create a new user with email authentication from the Firebase console:

  1. Navigate to Firebase Console: Firebase Console
    • Go to the Firebase Console.
    • Select the project you're working on
  2. Access Users Table:
    • On the left-hand side, click on "Authentication."
    • In the sub-menu, click on "Users."
  3. Add a New User:
    • Once you're on the "Users" page, click the "Add user" button.
  4. Enter User Details:
    • In the pop-up window, enter the user's email address and password.
    • The email and password can be temporary or fake, but it's essential to remember them if you want to test the sign-in functionality.
    • Note: If you use a fake email address, the "Forgot password" functionality won't work.
  5. Click "Add user":
    • After entering the user details, click on the "Add user" button to create the new user.
  6. Verify User Creation:
    • You should now see the newly created user in the list on the "Users" page.
      After successful login you will see the home page like this:

      Firebase auth in Flutter 05



Installing the dependencies for offline authentication


To implement offline authentication, we will be using the conneticityplus package, this package will help us to identify whether the user is offline or online.
Read more about this package here https://pub.dev/packages/connectivity_plus To install the package, run the below command

flutter pub add connectivity_plus

We will also be using flutter_scure_store to securely store the credentials Read more about this package here https://pub.dev/packages/flutter_secure_storage To install the package, run the below command

flutter pub add flutter_secure_storage

Add the secure storage


Create a new file and name it as secure_storage.dart

lib/secure_storage.dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorage {
  // Create storage
  final storage = const FlutterSecureStorage();

  final String _keyUserName = 'username';
  final String _keyPassWord = 'password';

  Future setUserName(String username) async {
    await storage.write(key: _keyUserName, value: username);
  }

  Future<String?> getUserName() async {
    return await storage.read(key: _keyUserName);
  }

  Future setPassWord(String password) async {
    await storage.write(key: _keyPassWord, value: password);
  }

  Future<String?> getPassWord() async {
    return await storage.read(key: _keyPassWord);
  }
}

Here, a class named SecureStorage is defined to handle secure storage using the FlutterSecureStorage package. The class contains methods to set and retrieve a username and password securely. It utilizes the FlutterSecureStorage instance to write and read data securely. Two private constants, _keyUserName and _keyPassWord, represent the keys for storing the username and password, respectively. The class provides methods such as setUserName, getUserName, setPassWord, and getPassWord to interact with the secure storage for managing user credentials.

Handle offline authentication


Now we will first check whether the user is offline or not. If the user is online we will proceed with Firebase authentication otherwise we validate the credentials with the secured storage credentials.

lib/authenticate.dart

 import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:myflutterapp/secure_storage.dart';
class Authentication {
  static Future<bool> isOnline() async {
    var connectivityResult = await Connectivity().checkConnectivity();
    return connectivityResult != ConnectivityResult.none;
  }

  static Future<AuthResponse?> authenticate(
      String email, String password) async {
    if (await isOnline()) {
      // Online authentication with Firebase
      try {
        UserCredential userCredential = await FirebaseAuth.instance
            .signInWithEmailAndPassword(email: email, password: password);
        return AuthResponse(
            username: email,
            userDetails: userCredential.user,
            isAuthenticated: true,
            message: "Login Success");
      } on FirebaseAuthException catch (e) {
        // Handle authentication errors
        print("Error: ${e.message}");
        return AuthResponse(
            username: email, isAuthenticated: false, message: e.message!);
      }
    } else {
      String? securedUsername = await SecureStorage().getUserName();
      String? securedPasswoed = await SecureStorage().getPassWord();
      if (securedPasswoed == password && securedUsername == email) {
        return AuthResponse(
            username: email,
            isAuthenticated: true,
            message: "Logged in successfully");
      } else {
        return AuthResponse(
            username: email,
            isAuthenticated: false,
            message:
                "Incorrect credentials! you may reset the password after you are online!");
      }
    }
  }
}

class AuthResponse {
  final String username;
  final User? userDetails;
  final bool isAuthenticated;
  final String message;

  AuthResponse(
      {required this.username,
      this.userDetails,
      required this.isAuthenticated,
      required this.message});
}

Here we first have added a method isOnline() this method checks if currently our user is offline or online.

 static Future<bool> isOnline() async {
    var connectivityResult = await Connectivity().checkConnectivity();
    return connectivityResult != ConnectivityResult.none;
  }

Then if the user is Offline, we validate the entered credentials with credentials stored in secure storage. If the credentials match, then we redirect users to the Home screen, else we present an incorrect password error. Next, we will have to store the credentials in the secure storage when the user logs in while they are online.

lib/login_screen.dart

 import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:myflutterapp/authenticate.dart';
import 'package:myflutterapp/home.dart';
import 'package:myflutterapp/secure_storage.dart'; //Importing the secure storage class

class LoginScreen extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  final SecureStorage _secureStorage = SecureStorage();//Initializing the SecureStorage class to use it later

  LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Flutter App'),
      ),
      body: Container(
        decoration: const BoxDecoration(
          color: Colors.blueGrey,
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextField(
                controller: emailController,
                decoration: InputDecoration(
                  labelText: 'Email',
                  filled: true,
                  fillColor: Colors.white,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                controller: passwordController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  filled: true,
                  fillColor: Colors.white,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                ),
                obscureText: true,
              ),
              const SizedBox(height: 32),
              ElevatedButton(
                onPressed: () async {
                  String email = emailController.text;
                  String password = passwordController.text;
                  try {
                    AuthResponse? user =
                        await Authentication.authenticate(email, password);

                    if (user != null && user.isAuthenticated) {
                //After successful login we are storing the
            // the credentials to the secured storage
                      await _secureStorage.setUserName(email);
                      await _secureStorage.setPassWord(password);

                      Navigator.pushReplacement(
                        context,
                        MaterialPageRoute(
                          builder: (context) => const MyHomePage(
                            title: "My Flutter App",
                          ),
                        ),
                      );
                    } else {
                      SnackBar snackBara = SnackBar(
                        content: Text(
                          user!.message,
                          style: const TextStyle(fontSize: 16),
                        ),
                        backgroundColor: Colors.red,
                      );

                      ScaffoldMessenger.of(context).showSnackBar(snackBara);
                    }
                  } on FirebaseAuthException catch (e) {
                    print("Error: ${e.message}");
                    const snackBar = SnackBar(
                      content: Text(
                        'Authentication failed. Please check your credentials.',
                        style: TextStyle(fontSize: 16),
                      ),
                      backgroundColor: Colors.red,
                    );

                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                },
                style: ElevatedButton.styleFrom(
                  primary: Colors.green,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12.0),
                  ),
                  padding:
                      const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
                ),
                child: const Text(
                  'Login',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
} 

Here, we are securely storing the user's email and password using an instance of the SecureStorage class.
await _secureStorage.setUserName(email); await _secureStorage.setPassWord(password);
These lines were executed after a successful authentication attempt, where the user's credentials were verified using the Authentication.authenticate method. If the authentication was successful, the user's email and password were stored securely using the _secureStorage instance, presumably for later use in the application. This secure storage approach enhanced the security of sensitive user information.

Testing and debugging


To run the application in the offline mode, open the Chrome dev tools, go to the network tab, and change throttling to offline. Now our application will run in the offline mode.

Firebase auth in Flutter 06


Now, while being in offline mode try to log in and you will see authentication happening correctly. You can also install the application on your device and then try to log in online mode and then log out. After that, you can turn off the internet connection and try to log in while being offline.
This is it; you have now successfully implemented offline authentication in your Flutter app.

Alternate approaches

The need for alternate approaches in offline authentication arises from the diverse security requirements and policies that vary across companies and applications. Here are some alternate approaches that you can follow depending on your use case and scenarios:
Hashing and salting passwords: Instead of storing plain passwords in secure storage, consider hashing and salting them before storage. This adds an extra layer of security by transforming the password into a unique hash using a salt key. Hashed passwords are more resistant to unauthorized access, even if the secure storage is compromised.
Biometric authentication: Implementing biometric authentication, such as fingerprint or face recognition, provides an additional layer of security. Biometric authentication can function both offline and online, making it a robust solution for user verification. However, integrating biometric features may require additional development effort.
Time-limited token authentication: Use time-limited authentication tokens that are generated and stored securely during online authentication. These tokens expire after a certain period, enhancing security. In offline mode, the app can validate the token's expiration locally, allowing users to access specific features without requiring server connectivity.
Device-level encryption: Leverage device-level encryption to secure sensitive data stored on the device. This ensures that even if the device is compromised, the stored authentication information remains encrypted and protected. Integrating device-level encryption adds an extra security layer to offline authentication.

Conclusion


Offline authentication plays a crucial role in enhancing the user experience, accessibility, and security of Flutter applications, especially in scenarios with limited or no internet connectivity. The ability to authenticate users offline ensures uninterrupted access to app features, reduces dependency on servers, and complies with regulatory standards. In this comprehensive guide, we explored the implementation of offline authentication in Flutter projects using Firebase Authentication. The step-by-step process involved setting up a Flutter project, configuring Firebase for authentication, and integrating the necessary dependencies like connectivity_plus and flutter_secure_storage. We organized the code structure, created a login screen, and implemented a secure storage mechanism for user credentials. The offline authentication logic was incorporated, allowing users to log in securely even when offline, with credentials verified using local storage. This approach not only ensures a seamless user experience but also adds an extra layer of security to the authentication process. Consideration of alternate approaches, such as hashing passwords and implementing biometric authentication,