Email Authentication with Flutter — Firebase

Abhishek Doshi
Somnio Software — Flutter Agency
10 min readMar 24, 2022

Authentication is one of the basic functionalities every app should have. Here’s how to add authentication to your Flutter app using Firebase Auth!

Firebase is one of the go-to Back-End with Flutter apps since it provides many free functionalities as well as great integration with Flutter. One of the features provided by Firebase is Authentication. This way we can integrate Email, Phone, Google, Apple, and many more authentication in our apps!

To give you a quick sneak-peak of the article, we will be using firebase auth package for Firebase Authentication. This article was created with an example run on Flutter 2.10 version!

So, let’s get started with the basic Email/Password Authentication!

Step 0: Create a private package for auth.

Packages in Flutter are libraries of code that can be shared among projects and are independent of the project that developers incorporate and reuse to make work easy and less time-consuming.

Creating a package for Firebase Auth is a very good option thereby reducing package dependencies on our main project.

So, to create a package, we must first create a folder called packages inside our project. Then, in that location, run the following command:

flutter create --template=package auth_service

The above command will create a folder named auth_service inside packages. When you run this command in the terminal, you will find that certain files were created:

Once these files are created, you will find a folder inside packages named auth_service:

Step 1: Enable Authentication from Firebase Console and select Email/Password.

Once you have added your project and app to Firebase Console, the pre-requisite step is to enable Authentication from the right panel of the console and enable Email/Password from it.

When you click on Authentication, you will get a welcome screen from where you can click on Get Started

When you click on the button, you will be redirected to Sign In Method list. From there you can enable Email/Password.

Step 2: Create a simple UI for registration and login.

You can create a simple UI with 2 TextFields, one for Email Address, one for Password, and a button to submit. Note: It’s a good practice to use StatelessWidget instead of StatefulWidget as its rebuild costs less compared to StatefulWidget.

Here’s an example:

login_view.dart

import 'package:auth_example/signup/view/signup_view.dart';
import 'package:auth_example/home/view/home_view.dart';
import 'package:flutter/material.dart';
class LoginView extends StatelessWidget {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_LoginEmail(emailController: _emailController),
const SizedBox(height: 30.0),
_LoginPassword(passwordController: _passwordController),
const SizedBox(height: 30.0),
_SubmitButton(
email: _emailController.text,
password: _passwordController.text,
),
const SizedBox(height: 30.0),
_CreateAccountButton(),
],
),
),
);
}
}
class _LoginEmail extends StatelessWidget {
_LoginEmail({
Key? key,
required this.emailController,
}) : super(key: key);
final TextEditingController emailController;@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: emailController,
decoration: const InputDecoration(hintText: 'Email'),
),
);
}
}
class _LoginPassword extends StatelessWidget {
_LoginPassword({
Key? key,
required this.passwordController,
}) : super(key: key);
final TextEditingController passwordController;@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
hintText: 'Password',
),
),
);
}
}
class _SubmitButton extends StatelessWidget {
_SubmitButton({
Key? key,
required this.email,
required this.password,
}) : super(key: key);
final String email, password;@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
print('hello');
},
child: const Text('Login'),
);
}
}
class _CreateAccountButton extends StatelessWidget {
const _CreateAccountButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SignUpView(),
),
);
},
child: const Text('Create Account'),
);
}
}

signup_view.dart

import 'package:auth_example/home/view/home_view.dart';
import 'package:flutter/material.dart';
class SignUpView extends StatelessWidget {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create Account'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_CreateAccountEmail(emailController: _emailController),
const SizedBox(height: 30.0),
_CreateAccountPassword(passwordController: _passwordController),
const SizedBox(height: 30.0),
_SubmitButton(
email: _emailController.text,
password: _passwordController.text,
),
],
),
),
);
}
}
class _CreateAccountEmail extends StatelessWidget {
_CreateAccountEmail({
Key? key,
required this.emailController,
}) : super(key: key);
final TextEditingController emailController;
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: emailController,
decoration: const InputDecoration(hintText: 'Email'),
),
);
}
}
class _CreateAccountPassword extends StatelessWidget {
_CreateAccountPassword({
Key? key,
required this.passwordController,
}) : super(key: key);
final TextEditingController passwordController;
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
hintText: 'Password',
),
),
);
}
}
class _SubmitButton extends StatelessWidget {
_SubmitButton({
Key? key,
required this.email,
required this.password,
}) : super(key: key);
final String email, password;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
print('hello');
},
child: const Text('Create Account'),
);
}
}

As you may see, this UI is pretty simple. Both screens have 2 textfields and a button. When clicking the button, we have just added print statements to print the email and password provided by the user.

Step 3: Create the Back-End code to pass the credentials to your Firebase.

The first thing you need to do is add 2 packages to your pubspec.yaml file (of the auth package you created); firebase_core and firebase_auth. Once that's done, run flutter pub get so that the Flutter Framework downloads the package content to your local system.

Now, you need to initialize the Firebase App in your main() function which we have in main.dart file:

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

The 1st line, WidgetsFlutterBinding.ensureInitialized(); ensures that the UI is not rendered until our initialization is not done.

Now, let’s create a file to contain our authentication related functions and Firebase calls. Let’s name it firebase_auth_service.dart inside our auth package. Next step is to create 2 functions: login and register.

Future<UserEntity?> signInWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
final userCredential = await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
return _mapFirebaseUser(userCredential.user);
} on auth.FirebaseAuthException catch (e) {
throw _determineError(e);
}
}
@override
Future<UserEntity?> createUserWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
final userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return _mapFirebaseUser(_firebaseAuth.currentUser);
} on auth.FirebaseAuthException catch (e) {
throw _determineError(e);
}
}

So, as we are using Firebase for Authentication, we have pre-built methods for login and register. For register, we can use createUserWithEmailAndPassword and it takes 2 parameters i.e. email and password. Similarly, we can use signInWithEmailAndPassword which also takes 2 parameters i.e. email and password. Here, we created two user-defined methods to handle our Firebase calls. These methods take email and password as the parameters and pass them to the Firebase functions.

Here, if you notice, we are returning UserEntity. We have just created a simple model class which looks like this:

import 'package:equatable/equatable.dart';class UserEntity extends Equatable {
const UserEntity({
required this.id,
required this.firstName,
required this.lastName,
required this.email,
required this.imageUrl,
});
final String id;
final String firstName;
final String lastName;
final String email;
final String imageUrl;
factory UserEntity.fromJson(Map<String, dynamic> json) => UserEntity(
id: json['id'] ?? "",
firstName: json['firstName'] ?? "",
lastName: json['lastName'] ?? "",
email: json['email'] ?? "",
imageUrl: json['imageUrl'] ?? "",
);
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'firstName': firstName,
'lastName': lastName,
'email': email,
'imageUrl': imageUrl,
};
factory UserEntity.empty() => const UserEntity(
id: "",
firstName: "",
lastName: "",
email: "",
imageUrl: "",
);
@override
List<Object?> get props => [id, firstName, lastName, email, imageUrl];
}

We have also created a method called _determineError which determines which error was thrown. It's always a good practice to handle exceptions so that our code doesn't break! Here's the code for it:

AuthError _determineError(auth.FirebaseAuthException exception) {
switch (exception.code) {
case 'invalid-email':
return AuthError.invalidEmail;
case 'user-disabled':
return AuthError.userDisabled;
case 'user-not-found':
return AuthError.userNotFound;
case 'wrong-password':
return AuthError.wrongPassword;
case 'email-already-in-use':
case 'account-exists-with-different-credential':
return AuthError.emailAlreadyInUse;
case 'invalid-credential':
return AuthError.invalidCredential;
case 'operation-not-allowed':
return AuthError.operationNotAllowed;
case 'weak-password':
return AuthError.weakPassword;
case 'ERROR_MISSING_GOOGLE_AUTH_TOKEN':
default:
return AuthError.error;
}
}
}

Here, AuthError is just an enum:

enum AuthError {
invalidEmail,
userDisabled,
userNotFound,
wrongPassword,
emailAlreadyInUse,
invalidCredential,
operationNotAllowed,
weakPassword,
error,
}

Now, let’s call these methods from our UI!

Step 4: Call functions from UI!

The next step is to call the functions from our UI. Now, to use the auth package that we created in our app, you can add it to your app’s pubspec.yaml and then import it wherever needed.

auth: 
path: packages/auth

Now, for our UI, currently we just added print statements when the user clicks on the buttons of login and register. Now, it’s time for some action!

onPressed: () async {
try {
await _authService.createUserWithEmailAndPassword(
email: _emailController.text,
password: _passwordController.text,
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => Home()));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
},

Here, the above code is for create_account page. Here, we have called the createUserWithEmailAndPassword which we created in the auth_service.dart and if we don't get any exception, we are navigating to home screen or show SnackBar if there's any error. Same way, we can do for login too. So your final code will look like the following:

signup_view.dart

import 'package:auth_example/home/view/home_view.dart';
import 'package:auth_service/auth.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class SignUpView extends StatelessWidget {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create Account'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_CreateAccountEmail(emailController: _emailController),
const SizedBox(height: 30.0),
_CreateAccountPassword(passwordController: _passwordController),
const SizedBox(height: 30.0),
_SubmitButton(
email: _emailController.text,
password: _passwordController.text,
),
],
),
),
);
}
}
class _CreateAccountEmail extends StatelessWidget {
_CreateAccountEmail({
Key? key,
required this.emailController,
}) : super(key: key);
final TextEditingController emailController;
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: emailController,
decoration: const InputDecoration(hintText: 'Email'),
),
);
}
}
class _CreateAccountPassword extends StatelessWidget {
_CreateAccountPassword({
Key? key,
required this.passwordController,
}) : super(key: key);
final TextEditingController passwordController;
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
hintText: 'Password',
),
),
);
}
}
class _SubmitButton extends StatelessWidget {
_SubmitButton({
Key? key,
required this.email,
required this.password,
}) : super(key: key);
final String email, password;
final AuthService _authService = FirebaseAuthService(
authService: FirebaseAuth.instance,
);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
try {
await _authService.createUserWithEmailAndPassword(
email: email,
password: password,
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => Home(),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
},
child: const Text('Create Account'),
);
}
}

Output:

login_view.dart

import 'package:auth_example/signup/view/signup_view.dart';
import 'package:auth_example/home/view/home_view.dart';
import 'package:auth_service/auth.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class LoginView extends StatelessWidget {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_LoginEmail(emailController: _emailController),
const SizedBox(height: 30.0),
_LoginPassword(passwordController: _passwordController),
const SizedBox(height: 30.0),
_SubmitButton(
email: _emailController.text,
password: _passwordController.text,
),
const SizedBox(height: 30.0),
_CreateAccountButton(),
],
),
),
);
}
}
class _LoginEmail extends StatelessWidget {
_LoginEmail({
Key? key,
required this.emailController,
}) : super(key: key);
final TextEditingController emailController;@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: emailController,
decoration: const InputDecoration(hintText: 'Email'),
),
);
}
}
class _LoginPassword extends StatelessWidget {
_LoginPassword({
Key? key,
required this.passwordController,
}) : super(key: key);
final TextEditingController passwordController;@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
hintText: 'Password',
),
),
);
}
}
class _SubmitButton extends StatelessWidget {
_SubmitButton({
Key? key,
required this.email,
required this.password,
}) : super(key: key);
final String email, password;
final AuthService _authService = FirebaseAuthService(
authService: FirebaseAuth.instance,
);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
try {
await _authService.signInWithEmailAndPassword(
email: email,
password: password,
);
Navigator.of(context)
.pushReplacement(MaterialPageRoute(builder: (context) => Home()));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
},
child: const Text('Login'),
);
}
}
class _CreateAccountButton extends StatelessWidget {
const _CreateAccountButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SignUpView(),
),
);
},
child: const Text('Create Account'),
);
}
}

Output:

So, now this way we can use the auth_service package in any project that we want. And if at any point in the future we want to change the Back-End from Firebase to another service, we just need to update this package and our task is done! This allows us to make our app more scalable!

So, to summarize the steps:

  1. Add project and app to your Firebase Console.
  2. Enable Authentication and Email/Password Auth from console.
  3. Create a package for authentication service.
  4. Create simple UI to get email and password from user.
  5. Call these functions from the UI

Wait, did you like the article? We have more in our knowledge bag!

This is a beginner-friendly solution for you to easily incorporate and achieve an effective email authentication with Flutter — Firebase. However, to create a more scalable and robust product it would be necessary to use a State Management solution.

If you are interested, we are going to be sharing insight on how to build it for a high-quality product. So, stay tuned for our next articles 😉!

I hope you learned something new from this article. If you want to try it out, feel free to clone the GitHub Repository!

Abhishek Doshi is Google Developer Expert and is a Flutter Developer at Somnio Software. Abhi is an International Tech Speaker and Writer for Flutter and Firebase and enjoys helping grow this tech.

Originally published at https://somniosoftware.com.

--

--