One of the crucial parts of mobile app development is to perform authentication. It ensures that only authorized users will get the crucial information and perform tasks in the application. This article is all about Authentication With Firebase.

In this tutorial, we will explore how to perform secure authentication with Firebase in Flutter Application. Along with it, we will explore the BloC state management to handle the state of the application. By the end, you’ll have a solid concept and understanding of how to execute a firm authentication perform a secure sign-up process, and log in with Bloc.

Authentication With Firebase

Prerequisites:

To get the most out of this tutorial, you should have the following:

  • A good understanding of Flutter and Dart
  • A Firebase account: Create a Firebase account if you don’t have one. You can set up a Firebase project through this link.

What You’ll Build

In this article, you’ll build and Flutter application that allows users to log in or register using social IDs like Google, or a set of credentials, such as email and password. Your application will have the home screen and logout button.

 Authentication With Firebase

Working Mechanism of Firebase Authentication

Firebase Authentication is a great service with simplifies the process of authentication in your application. It supports multiple authentication methods, including Google, social media, email/password, and many more.

One of the main advantages of Firebase Authentication is its built-in security features like secure storage of user credentials as well as the encryption of sensitive information.

Flow chart

The image above is the flow chart to understand how it is going to work.

  • When the app is started, the Splash screen will appear. Here, we check if the user is logged in or not.
  • If the user is logged in, then we will redirect the user to the Home Screen.
  • Otherwise, if the user is not logged in, then we will redirect the user to the Auth Screen (Register/Login Screen).
  • Again when the user is registered or logged in, the user will be redirected to the Home Screen.

So this is going to be the simple mechanism, of how the application would behave.

Bloc State management

As you can see in the picture above.

  • First, we have the UI, and from the UI we request the bloc.
  • Bloc will have two things, the event and the state. First, when the UI connects to a bloc it creates and triggers events.
  • The event eventually calls the repositories to the server through an endpoint.
  • From the server now we get the data and it is passed back to the bloc. As we have the data we trigger the state.
  • As we have the change in the state UI knows from the Bloc pattern and updates the UI.
If you want to learn more about BloC visit here.

Let’s Dive In

To get started with Firebase Authentication, you must set up Firebase in your Flutter project.

Setup Firebase

Follow these instructions to set up Firebase in your projects.

Import Packages

Find the pubsec.yaml file, add the following dependencies.

dependencies:
  ...

  flutter_bloc: ^8.1.3
  equatable: ^2.0.5
  google_sign_in: ^6.2.1
  email_validator: ^2.1.17

Initialise Firebase

First, open your main.dart file inside the lib folder, add

To initialize Firebase, add the following code:

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

Set UpFirebase Authentication Bloc

A bloc contains events, states, and blocs. Firstly, let’s create states and events for the Bloc. After creating states and events, we’ll create an AuthenticationBloc which will operate the logic by using the events, states, and services we have created.

The AuthState class

This AuthState class is responsible for holding the state of the Authentication process. As in the code, there are loading, success, and failure states. These states ensure we know what is happening during the authentication process.

First, create an auth_state.dart file in your project’s bloc/auth directory.

@immutable
abstract class AuthState extends Equatable {}

class Loading extends AuthState {
  @override
  List<Object?> get props => [];
}

class Authenticated extends AuthState {
  @override
  List<Object?> get props => [];
}

class UnAuthenticated extends AuthState {
  @override
  List<Object?> get props => [];
}

class AuthError extends AuthState {
  final String error;

  AuthError(this.error);
  @override
  List<Object?> get props => [error];
}
Breaking Down Code

AuthState abstract class:

  • AuthState is the base class for multiple states where the authentication process can happen at any time.
  • All classes are state.

There are multiple states.

  • Loading:
    • Loading State holds the information that the user authentication process is going on.
  • Authenticated:
    • Authenticated State holds the information that the user authentication is successful.
  • UnAuthenticated:
    • UnAuthenticated State holds the information that the user is not logged in.
  • AuthError:
    • AuthError State holds the information of the errors that occurred during the registration or login process.

AuthEvent abstract class:

The AuthEvent is responsible for the events the AuthBlog performs.

Create auth_event.dart the class file inside your project’s bloc/auth directory.


import 'package:equatable/equatable.dart';

abstract class AuthEvent extends Equatable {
  @override
  List<Object> get props => [];
}


class SignInRequested extends AuthEvent {
  final String email;
  final String password;

  SignInRequested(this.email, this.password);
}


class SignUpRequested extends AuthEvent {
  final String email;
  final String password;

  SignUpRequested(this.email, this.password);
}

class GoogleSignInRequested extends AuthEvent {}

class SignOutRequested extends AuthEvent {}

In the code above there are multiple events the AuthBloc will perform.

  • SignInRequested
    • This event will get triggered when the user requests to register the account. It takes two string parameters. email and password.
  • SignUpRequested
    • It will get fired when the user tries to log in with an email and password.
  • GoogleSignInRequested
    • When the user tries to log in with a Google account, this event triggers.
  • SignOutRequested
    • When the user tries to log out from the system.
With every event, AuthRepository is called to perform requested task.

AuthBloc abstract class:

This AuthBloc will handle the overall activity, from what happens when the user clicks a button to what is displayed on the screen. It also communicates with the Firebase service we created.

Create auth_bloc.dart file inside your project’s bloc/auth directory.

Add the following code to define the AuthBloc class.

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository authRepository;

  AuthBloc({required this.authRepository}) : super(UnAuthenticated()) {
   
on<SignUpRequested>((event, emit) async {
      emit(Loading());
      try {
        await authRepository.signUp(
            email: event.email, password: event.password);
        emit(Authenticated());
      } catch (e) {
        emit(AuthError(e.toString()));
        emit(UnAuthenticated());
      }
    });
    
on<SignInRequested>((event, emit) async {
      emit(Loading());
      try {
        await authRepository.signIn(
            email: event.email, password: event.password);
        emit(Authenticated());
      } catch (e) {
        emit(AuthError(e.toString()));
        emit(UnAuthenticated());
      }
    });
    
    on<GoogleSignInRequested>((event, emit) async {
      emit(Loading());
      try {
        await authRepository.signInWithGoogle();
        emit(Authenticated());
      } catch (e) {
        emit(AuthError(e.toString()));
        emit(UnAuthenticated());
      }
    });

    on<SignOutRequested>((event, emit) async {
      emit(Loading());
      await authRepository.signOut();
      emit(UnAuthenticated());
    });
  }
}

In this code snippet, we have created an instance AuthRepository class, that handles user authentication tasks like signing up and signing out.

on<SignUpRequested>((event, emit) async {...} defines a handler for the SignUpRequested event. When this event gets triggered, the bloc passes through the following steps:

  • It emits a Loading State to indicate that the authentication process is going on.
  • It calls for the signUp method of authRepository to attempt to create a user account with the provided email and password.
  • If the user account creation is successful, it emits an Authenticated State.
  • If the user account creation fails, it emits an AuthError state with an error message followed by UnAuthenticated State.

All the process goes through the same states.

At the end on<SignOutRequested>((event, emit) async {…} define a handler for the SignOutRequested event. When this event gets triggered, the bloc passes through the following steps:

  • It emits a Loading State to indicate that the logout process is going on.
  • It calls for the signOut method of authRepository attempting to sign out from the account.
  • If the user account signout success, it emits an UnAuthenticated State.

How to Implement the Authentication With Firebase with Bloc

First, let’s create a file named signup_page.dart inside lib/pages the folder. Create a StatefulWidget named SignUp.

class SignUp extends StatefulWidget {
  const SignUp({Key? key}) : super(key: key);

  @override
  State<SignUp> createState() => _SignUpState();
}

class _SignUpState extends State<SignUp> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SignUp"),
      ),
      body: StreamBuilder<User?>(
          stream: FirebaseAuth.instance.authStateChanges(),
          builder: (context, snapshot) {
            // if (snapshot.hasData) {
              print("User Data email");
              print(snapshot.data!.email);
            // }
            return BlocConsumer<AuthBloc, AuthState>(
              listener: (context, state) {
                if (state is Authenticated) {
                  // Navigating to the dashboard screen if the user is authenticated
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute(
                      builder: (context) => const HomePage(),
                    ),
                  );
                }
                if (state is AuthError) {
                  // Displaying the error message if the user is not authenticated
                  ScaffoldMessenger.of(context)
                      .showSnackBar(SnackBar(content: Text(state.error)));
                }
              },
              builder: (context, state) {
                if (state is Loading) {
                  // Displaying the loading indicator while the user is signing up
                  return const Center(child: CircularProgressIndicator());
                }
                if (state is UnAuthenticated) {
                  // Displaying the sign up form if the user is not authenticated
                  return Center(
                    child: Padding(
                      padding: const EdgeInsets.all(18.0),
                      child: SingleChildScrollView(
                        reverse: true,
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            const Text(
                              "Sign Up",
                              style: TextStyle(
                                fontSize: 38,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(
                              height: 18,
                            ),
                            Center(
                              child: Form(
                                key: _formKey,
                                child: Column(
                                  children: [
                                    TextFormField(
                                      controller: _emailController,
                                      decoration: const InputDecoration(
                                        hintText: "Email",
                                        border: OutlineInputBorder(),
                                      ),
                                      autovalidateMode:
                                          AutovalidateMode.onUserInteraction,
                                      validator: (value) {
                                        return value != null &&
                                                !EmailValidator.validate(value)
                                            ? 'Enter a valid email'
                                            : null;
                                      },
                                    ),
                                    const SizedBox(
                                      height: 10,
                                    ),
                                    TextFormField(
                                      controller: _passwordController,
                                      decoration: const InputDecoration(
                                        hintText: "Password",
                                        border: OutlineInputBorder(),
                                      ),
                                      autovalidateMode:
                                          AutovalidateMode.onUserInteraction,
                                      validator: (value) {
                                        return value != null && value.length < 6
                                            ? "Enter min. 6 characters"
                                            : null;
                                      },
                                    ),
                                    const SizedBox(
                                      height: 12,
                                    ),
                                    SizedBox(
                                      width: MediaQuery.of(context).size.width *
                                          0.7,
                                      child: ElevatedButton(
                                        onPressed: () {
                                          _createAccountWithEmailAndPassword(
                                              context);
                                        },
                                        child: const Text('Sign Up'),
                                      ),
                                    )
                                  ],
                                ),
                              ),
                            ),
                            const Text("Already have an account?"),
                            OutlinedButton(
                              onPressed: () {
                                Navigator.pushReplacement(
                                  context,
                                  MaterialPageRoute(
                                      builder: (context) => const SignIn()),
                                );
                              },
                              child: const Text("Sign In"),
                            ),
                            const Text("Or"),
                            InkWell(
                              onTap: () {
                                _authenticateWithGoogle(context);
                              },
                              child: Card(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.center,
                                      children: [
                                        Image.asset(
                                          "assets/google.png",
                                          height: 30,
                                          width: 30,
                                        ),
                                        const SizedBox(
                                          width: 10,
                                        ),
                                        const Text("Continue with Google")
                                      ]),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  );
                }
                return Container();
              },
            );
          }),
    );
  }

  void _createAccountWithEmailAndPassword(BuildContext context) {
    if (_formKey.currentState!.validate()) {
      BlocProvider.of<AuthBloc>(context).add(
        SignUpRequested(
          _emailController.text,
          _passwordController.text,
        ),
      );
    }
  }

  void _authenticateWithGoogle(context) {
    BlocProvider.of<AuthBloc>(context).add(
      GoogleSignInRequested(),
    );
  }
}

So this code creates a simple SignUp screen that consists of two textfields email and password. It also consists of two ElevatedButton. The BlocConsumer widget wraps the body items and listens for AuthBloc state changes. When a user clicks the button, it triggers an event to AuthBloc to start the user registration process.

  • If the state is Authenticated, we navigate to the HomePage.
  • If the state is AuthError, we display a SnackBar with an error message.
  • If the state is Loading, we show CircularProgressIndicator.
  • If the state is UnAuthenticated, the SignUp screen elements are shown.
  • Else we are going to return an empty Container.

builder: Builds the UI based on the current state received from the AuthBloc. As per the authentication state, this button may display different feedback, navigate to another screen, or show different views.

 void _createAccountWithEmailAndPassword(BuildContext context) {
    if (_formKey.currentState!.validate()) {
      BlocProvider.of<AuthBloc>(context).add(
        SignUpRequested(
          _emailController.text,
          _passwordController.text,
        ),
      );
    }
  }

_createAccountWithEmailAndPassword this method is called when a user presses the Sign Up button. It takes a parameter BuildContext.

if (_formKey.currentState!.validate()) {} this means if the email and password fields are not empty we are going to call the AuthBloc‘s SignUpRequested with the inputs of email and password textfields.

 validator: (value) {
                                        return value != null &&
                                                !EmailValidator.validate(value)
                                            ? 'Enter a valid email'
                                            : null;
                                      }
validator: (value) {
                                        return value != null && value.length < 6
                                            ? "Enter min. 6 characters"
                                            : null;
                                      },

If the email and password fields are empty, then this validator comes to an action. It will display the error messages if certain defined criteria are not fulfilled.

 void _authenticateWithGoogle(context) {
    BlocProvider.of<AuthBloc>(context).add(
      GoogleSignInRequested(),
    );
  }

void _authenticateWithGoogle(context){...} this method is called when the user presses the Continue with Google button.

To make the Firebase accepting Email Password authentication and Google login you need to go to your Firebase Console and select your project. Inside the project find Authentication.

Go to Sign-in method.

  • Click on Add new Provider. Enable Email/Password and Google.

LoginPage

Let’s create a file named signin_page.dart inside lib/pages the folder. Create a StatefulWidget named SignIn.

class SignIn extends StatefulWidget {
  const SignIn({Key? key}) : super(key: key);

  @override
  State<SignIn> createState() => _SignInState();
}

class _SignInState extends State<SignIn> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SignIn"),
      ),
      body: BlocListener<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is Authenticated) {
            // Navigating to the dashboard screen if the user is authenticated
            Navigator.pushReplacement(context,
                MaterialPageRoute(builder: (context) => const HomePage()));
          }
          if (state is AuthError) {
            // Showing the error message if the user has entered invalid credentials
            ScaffoldMessenger.of(context)
                .showSnackBar(SnackBar(content: Text(state.error)));
          }
        },
        child: BlocBuilder<AuthBloc, AuthState>(
          builder: (context, state) {
            if (state is Loading) {
              // Showing the loading indicator while the user is signing in
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
            if (state is UnAuthenticated) {
              // Showing the sign in form if the user is not authenticated
              return Center(
                child: Padding(
                  padding: const EdgeInsets.all(18.0),
                  child: SingleChildScrollView(
                    reverse: true,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text(
                          "Sign In",
                          style: TextStyle(
                            fontSize: 38,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(
                          height: 18,
                        ),
                        Center(
                          child: Form(
                            key: _formKey,
                            child: Column(
                              children: [
                                TextFormField(
                                  keyboardType: TextInputType.emailAddress,
                                  controller: _emailController,
                                  decoration: const InputDecoration(
                                    hintText: "Email",
                                    border: OutlineInputBorder(),
                                  ),
                                  autovalidateMode:
                                      AutovalidateMode.onUserInteraction,
                                  validator: (value) {
                                    return value != null &&
                                            !EmailValidator.validate(value)
                                        ? 'Enter a valid email'
                                        : null;
                                  },
                                ),
                                const SizedBox(
                                  height: 10,
                                ),
                                TextFormField(
                                  keyboardType: TextInputType.text,
                                  controller: _passwordController,
                                  decoration: const InputDecoration(
                                    hintText: "Password",
                                    border: OutlineInputBorder(),
                                  ),
                                  autovalidateMode:
                                      AutovalidateMode.onUserInteraction,
                                  validator: (value) {
                                    return value != null && value.length < 6
                                        ? "Enter min. 6 characters"
                                        : null;
                                  },
                                ),
                                const SizedBox(
                                  height: 12,
                                ),
                                SizedBox(
                                  width:
                                      MediaQuery.of(context).size.width * 0.7,
                                  child: ElevatedButton(
                                    onPressed: () {
                                      _authenticateWithEmailAndPassword(
                                          context);
                                    },
                                    child: const Text('Sign In'),
                                  ),
                                )
                              ],
                            ),
                          ),
                        ),
                        
                      const SizedBox(height: 10,),
                        InkWell(
                           onTap: () {
                            _authenticateWithGoogle(context);
                          },
                          child: Card(child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                              Image.asset(
                                "assets/google.png",
                                height: 30,
                                width: 30,
                              ),
                              const SizedBox(width: 10,),
                             const Text("Continue with Google")
                            ]),
                          ),),
                        ),
                      const SizedBox(height: 10,),
                        const Text("Don't have an account?"),
                        
                      const SizedBox(height: 10,),
                        OutlinedButton(
                          onPressed: () {
                            Navigator.pushReplacement(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => const SignUp()),
                            );
                          },
                          child: const Text("Sign Up"),
                        )
                      ],
                    ),
                  ),
                ),
              );
            }
            return Container();
          },
        ),
      ),
    );
  }

  void _authenticateWithEmailAndPassword(context) {
    if (_formKey.currentState!.validate()) {
      BlocProvider.of<AuthBloc>(context).add(
        SignInRequested(_emailController.text, _passwordController.text),
      );
    }
  }

  void _authenticateWithGoogle(context) {
    BlocProvider.of<AuthBloc>(context).add(
      GoogleSignInRequested(),
    );
  }
}

So, this returns this screen.

Authentication With Firebase

As similar to SignUp, works similarly.

Opposite of SignUp, when a user presses the button, it triggers this method:

void _authenticateWithEmailAndPassword(context) {
    if (_formKey.currentState!.validate()) {
      BlocProvider.of<AuthBloc>(context).add(
        SignInRequested(_emailController.text, _passwordController.text),
      );
    }
  }

This method requests for SignInRequested with email and password.

HomePage

Let’s create a file named homepage.dart inside lib/pages the folder. Create a StatefulWidget named HomePage.

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

@override
  Widget build(BuildContext context) {
 
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firestore'),
        actions: [
          InkWell(
              onTap: ()  {
                context.read<AuthBloc>().add(SignOutRequested());
              },
              child: const Text("Logout"))
        ],
      ),
      body: Container()
    );
  }

  }

In the AppBar you can see there is a action Text widget named Logout, when clicked will call the SignOutRequested from AuthBloc.And user will be logged out the system.

To implement the authentication flow and continue user process with the state, inside the main.dart, you have MyApp Stateless widget.

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider(
            create: (context) => AuthRepository(),
      child: MultiBlocProvider(
        providers: [
          BlocProvider(
              create: (context) => AuthBloc(
                    authRepository:
                        RepositoryProvider.of<AuthRepository>(context),
                  )),
        ],
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: StreamBuilder<User?>(
              stream: FirebaseAuth.instance.authStateChanges(),
              builder: (context, snapshot) {
                // If the snapshot has user data, then they're already signed in. So Navigating to the Dashboard.
                if (snapshot.hasData) {
                  return const HomePage();
                }
                // Otherwise, they're not signed in. Show the sign in page.
                return const SignIn();
              }),
        ),
      ),
    );
  }
}

In the code above, you have MyApp StatelessWidget with a RepositoryProvider where we create the AuthRepository. RepositoryProvider consists of a child MultiBlocProvider where we register the AuthBloc. And the child of MultiBlocProvider MaterialApp has a home with an StreamBuilder as the child. The StreamBuilder acts as a judge, using Firebase to check the state changes and if a user has logged in or not. If a user has logged in, it directs them to the home screen, else it goes to the sign-up screen.

Run

When completed you can now run your Flutter app and test the registration, login, and logout functionalities. If you want to clone the repo, you can check it out on GitHub here and leave a like.

Conclusion

In this article, we explored building a user authentication flow in Flutter using Firebase for authentication and the Bloc state management pattern for handling application state.

We learned how to set up Firebase in a Flutter project, create Blocs for authentication, and implement the authentication flow using Bloc.

By leveraging the power of Firebase and the predictability of Bloc, you can ensure a secure and seamless user authentication experience in your Flutter apps.

Thanks for reading this article.

Also, follow to get updated on exciting articles and projects.

If I got something wrong? Let me know in the comments. I would love to improve.

Let’s get connected

We can be friends. Find on FacebookLinkedinGithubYouTube

BuyMeACoffee, and Instagram.

Contribute: BuyMeACoffee

ContactContact Us

Leave a Reply

Your email address will not be published. Required fields are marked *