Securing Access: Building a Firebase-powered Login Page with Email Authentication and Authentication Guard (Part 8) in Your Angular-15 Ionic-7 App
Welcome back to the eighth part of our series on building a mobile app with Angular 15 and Ionic 7. In this post, we will learn how to create a login page with Firebase authentication using an email provider and authentication guard.
Firebase is a platform that provides various services for web and mobile development, such as hosting, database, storage, analytics, and authentication. Authentication is the process of verifying the identity of a user who wants to access a protected resource, such as an app or a website.
Firebase authentication offers several methods to authenticate users, such as email and password, phone number, Google, Facebook, Twitter, and more. In this tutorial, we will use the email and password method, which allows users to sign up and sign in with their email address and a password of their choice.
We will also use an authentication guard, which is a service that checks if a user is logged in before allowing them to access certain pages of our app. This way, we can protect our app from unauthorized access and provide a better user experience. Let's get started!
I have used Google’s Firebase service for authentication on the application which will allow users to enter their e-mail and password to create an new account or login into an existing one. I have used the following Firebase official documentation to understand and implement the steps in the latest versions:
Add Firebase to your JavaScript project
First, I will create a firebase account using my existing Google account. Then I will add a new project to the firebase console by entering the project details. For the moment, I will keep the environment to be under testing and not under production. Figure 23 shows the main home page of firebase console where all projects are listed:
Figure 23: Firebase console with project listings |
Next, I register the by adding a web app to the project which will give me the web app credentials such as the API key.
Once the app is created in Firebase, I will use the credentials under the firebaseConfig
object in my project files to access the firebase authentication services. Next, I will install firebase using the command:
npm install firebase
Then I will add the official Angular library for Firebase from this documentation:
angular/angularfire: The official Angular library for Firebase. (github.com)
And install the above angular library for firebase using the command:
npm install @angular/fire
Next, I will enter the configuration from project settings to the environment.ts
file in the following way:
export const environment = {
production: false,
...
//firebase API key
firebase: {
apiKey: 'MY-API-KEY',
authDomain: 'grange-mobile-karan.firebaseapp.com',
projectId: 'grange-mobile-karan',
storageBucket: 'grange-mobile-karan.appspot.com',
messagingSenderId: 'MY-SENDER-ID',
appId: 'MY-APP-ID',
},
...
};
When the packages are installed, the specific modules are generally automatically imported into the main.ts
providers array, but if not, can be added manually in the following way with the link to firebase API from environment file:
...
//firebase imports for angular fire for authentication, firestore and storage
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
import { getStorage, provideStorage } from '@angular/fire/storage';
import {
FacebookAuthProvider,
GoogleAuthProvider,
TwitterAuthProvider,
getAuth,
provideAuth,
} from '@angular/fire/auth';
...
if (environment.production) {
enableProdMode();
}
bootstrapApplication(AppComponent, {
providers: [
AuthGuard,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
importProvidersFrom(
...
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideFirestore(() => getFirestore()),
provideStorage(() => getStorage()),
provideAuth(() => getAuth()),
AuthGuard
),
provideRouter(routes),
],
});
Next, I will create a service to access the firebase service to create the register and login services. I use the following command to create the authentication service:
ionic generate service services/auth
Now, in the auth.service.ts
file, I will import the authentication service from angular fire and create the logic to register a new user and login to an existing one in the following way:
import { Injectable } from '@angular/core';
import {
Auth, createUserWithEmailAndPassword, signInWithEmailAndPassword,signOut,
} from '@angular/fire/auth';
@Injectable({
providedIn: 'root',
})
export class AuthService {
constructor(private auth: Auth) {} //inject the auth service
async register({ email, password }: { email: string; password: string }) {
try {
//creating a try catch block to catch any errors
const user = await createUserWithEmailAndPassword(
this.auth, email, password
); //if the user is registered, return the user
return user;
} catch (e) {
return null; //if the user is not registered, return null
}
}
// Binding element 'email' implicitly has an 'any' type.ts(7031). Resolve this error by adding a type annotation to the binding element.
async login({ email, password }: { email: string; password: string }) {
// creating a try catch block to catch any errors
try {
const user = await signInWithEmailAndPassword(this.auth, email, password);
// if the user is logged in, return the user
return user;
} catch (e) {
return null; // if the user is not logged in, return null
}
}
async logout() {
//sign out the user
return await signOut(this.auth);
}
}
The above code exports a class called AuthService
that contains methods for registering, logging in, and logging out a user. The constructor of the class takes in an instance of the Auth
service from Firebase Authentication, which is used to interact with the Firebase Authentication API.
The register
method is an asynchronous function that takes an object with an email
and password
property. It uses the createUserWithEmailAndPassword
method from Firebase Authentication to create a new user with the given email and password. If the user is successfully registered, the method returns the user
object; otherwise, it returns null
.
The login
method is similar to the register
method but takes an object with an email
and password
property as its parameter. It uses the signInWithEmailAndPassword
method from Firebase Authentication to authenticate the user with the given email and password. If the user is successfully authenticated, the method returns the user
object; otherwise, it returns null
.
The logout
method is an asynchronous function that signs out the current user using the signOut
method from Firebase Authentication. It returns a promise that resolves when the sign-out process is complete.
Note that the email
and password
parameters in the register
and login
methods are explicitly typed as strings using the TypeScript syntax of : { email: string; password: string }
. This is to prevent the TypeScript compiler from throwing an error related to implicit any
types.
Next, I will use this service in a newly created login page which will also act as an authentication guard to the tab4 page which displays user profile.
ionic generate page pages/login
I could’ve also applied the page as authentication guard on the entire app which is what happens in real-world applications but for convenience of testing, I’ve add the login page only to the profile page.
First, I will shift the rout path of login page from app.route.ts
to tabs.route.ts
and then, I will use the authentication guard service to create a logic in the tabs.route.ts
which will allow a user to enter the profile page only if the user is authenticated, in the following way:
import { Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
import {
redirectUnauthorizedTo,
canActivate,
redirectLoggedInTo,
} from '@angular/fire/auth-guard';
const redirectUnauthorizedToLogin = () =>
redirectUnauthorizedTo(['/tabs/login']); //if not logged in, redirect to login page
const redirectLoggedInToHome = () => redirectLoggedInTo(['tabs/tab4']); //if logged in, redirect to tabs page
export const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
...,
{
path: 'tab4',
loadComponent: () =>
import('../tab4/tab4.page').then((m) => m.Tab4Page),
...canActivate(redirectUnauthorizedToLogin),
},
{
path: 'login',
loadComponent: () =>
import('../login/login.page').then((m) => m.LoginPage),
...canActivate(redirectLoggedInToHome),
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
...
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
];
In the above code, the TabsPage
component has several child routes for multiple pages, each of which corresponds to a different tab in the application. Each child route is defined as an object that specifies the path
to the route, the loadComponent
function that lazily loads the component for the route, and a canActivate
property that specifies the guard function to run before allowing access to the route. In this case, the redirectUnauthorizedToLogin
function is used as the guard function for the 'tab4'
route, which means that the user will be redirected to the login page if they are not logged in. Similarly, the redirectLoggedInToHome
function is used as the guard function for the 'login'
route, which means that the user will be redirected to the home page if they are already logged in. The redirectUnauthorizedTo
and redirectLoggedInTo
functions are imported from the @angular/fire/auth-guard
module, which is used to guard access to routes based on a user's authentication status.
Once the guard is in place, whenever the user clicks on the tab4
i.e. profile
page from bottom navigation, they will land on the login page if they are not authenticated previously.
Now, I will use the authentication service to create the logic for the login page in login.page.ts
file in the following way:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule, ToastController, isPlatform } from '@ionic/angular';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LoadingController, AlertController } from '@ionic/angular';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth.service';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, FormsModule, ReactiveFormsModule],
})
export class LoginPage implements OnInit {
credentials!: FormGroup; // form group for the login form fields (email and password)
constructor(
private fb: FormBuilder,
private loadController: LoadingController,
private alertController: AlertController,
private router: Router,
private authService: AuthService,
private toastCtrl: ToastController
) { }
public get email() {
return this.credentials.get('email');
}
public get password() {
return this.credentials.get('password');
}
ngOnInit() {
// create a form group for the login form
this.credentials = this.fb.group({
email: ['', [Validators.required, Validators.email]], // email field
password: ['', [Validators.required, Validators.minLength(6)]], // password field
});
}
async register() {
// register the user
const loading = await this.loadController.create({
// create a loading controller
message: 'Registering...',
spinner: 'crescent', // spinner type
showBackdrop: true,
});
await loading.present(); // present the loading controller
const user = await this.authService.register(this.credentials.value); // register the user
await loading.dismiss(); // dismiss the loading controller
if (user) {
// if the user is registered, navigate to the profile page
this.router.navigateByUrl('/tabs/tab4', { replaceUrl: true });
const toast = await this.toastCtrl.create({
message: 'Registration Successful.',
duration: 3000,
position: 'bottom',
});
await toast.present();
} else {
// if the user is not registered, show an alert message to the user to try again
const alert = await this.alertController.create({
header: 'Sorry, Registration Failed',
message:
'Please try again. Please note that you can only register with a valid email address and password. If you have already registered, please login.',
buttons: ['OK'],
});
await alert.present(); // present the alert message
}
}
async login() {
// login the user
const loading = await this.loadController.create({
// create a loading controller
message: 'Logging in...',
spinner: 'crescent', // spinner type
showBackdrop: true,
});
await loading.present(); // present the loading controller
const user = await this.authService.login(this.credentials.value); // login the user
await loading.dismiss(); // dismiss the loading controller
if (user) {
this.router.navigateByUrl('/tabs/tab4', { replaceUrl: true });
console.log(user);
const toast = await this.toastCtrl.create({
message: 'Login Successful. Welcome to Your Profile Page!',
duration: 3000,
position: 'bottom',
});
await toast.present();
} else {
// if the user is not logged in, show an alert message to the user to try again
const alert = await this.alertController.create({
header: 'Sorry, Login Failed',
message:
'Please try again. If you are not registered, please register first. Please note that you can only register with a valid email address and password. ',
buttons: ['OK'],
});
await alert.present(); // present the alert message
}
}
}
The above code defines a class called LoginPage
that implements the OnInit
interface. It has a constructor that takes several Angular service dependencies as arguments. These dependencies are used in the register()
and login()
methods of the class.
It also has a credentials
property that is of type FormGroup
. This is a property that is used to store the values of the login form fields (email and password). The ngOnInit()
method of the class creates this FormGroup
using the FormBuilder
service. This service is used to create complex forms in Angular.
The LoginPage
class has two additional methods called register()
and login()
. These methods are used to register and login the user respectively. Both of these methods first create a loading spinner using the LoadingController
service, which is used to show a spinner to the user while the registration or login process is ongoing.
After creating the loading spinner, the register()
and login()
methods call methods on the AuthService
service to register or login the user respectively. If the user is successfully registered or logged in, the methods navigate the user to the profile page using the Router
service. The methods also show a success toast message using the ToastController
service.
If the registration or login process fails, the methods show an error message using the AlertController
service. The error message informs the user that the registration or login process has failed and prompts them to try again.
I referred to the following link to build this authentication feature:
Get Started with Firebase Authentication on Websites
Next, I will use the login methods and display them on the template file which is login.page.html
in the following way:
<ion-header [translucent]="false">
<ion-toolbar>
<ion-title>Login/Sign Up</ion-title>
<ion-progress-bar [value]="progress"></ion-progress-bar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-menu-button menu="main-menu"></ion-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true" class="ion-padding">
<form [formGroup]="credentials" (ngSubmit)="login()">
<ion-input
class="ion-margin-top"
fill="outline"
label="E-Mail"
label-placement="floating"
required
formControlName="email"
type="email"
></ion-input>
<ion-note
color="danger"
slot="error"
*ngIf="email && (email.dirty || email.touched) && email.errors"
>
Email is Invalid
</ion-note>
<ion-input
class="ion-margin-top"
label-placement="floating"
fill="outline"
label="Password"
required
formControlName="password"
type="password"
></ion-input>
<ion-note
color="danger"
slot="error"
*ngIf="password && (password.dirty || password.touched) && password.errors"
>
Password is Invalid. It needs to be 6 characters.
</ion-note>
<ion-button
class="ion-margin-top"
expand="full"
shape="round"
color="primary"
type="submit"
[disabled]="credentials.invalid"
[strong]="true"
>
Login
</ion-button>
<ion-button
fill="outline"
class="ion-margin-top"
expand="full"
shape="round"
color="primary"
type="button"
[strong]="true"
(click)="register()"
>
Create Account
</ion-button>
</form>
</ion-content>
The above code uses a form that contains two input fields for the user to enter their email and password. The formGroup
attribute is set to credentials
, which refers to a FormGroup
object defined in the previous login.page.ts
TypeScript code. The input fields are decorated with several Ionic attributes, such as fill
, label
, label-placement
, and type
. There are also ion-note
elements included that will display error messages if the user inputs an invalid email or password. The code also includes two Ionic buttons for the user to submit the form or create a new account. The disabled
attribute is set to credentials.invalid
, which prevents the user from submitting the form if the input fields are not valid.
The ngSubmit
event binding is used as an inbuilt angular directive that calls the submit method from the login button that has the type submit by default.
In the above code, the *ngIf
directive is used to conditionally render the <ion-note>
element based on the status of the email
form control. The condition inside the directive is evaluated as follows:
email
: refers to theFormControl
object namedemail
.email.dirty
: a property of theFormControl
object that indicates whether the control has been changed.email.touched
: another property of theFormControl
object that indicates whether the control has been blurred (i.e., the user has interacted with it and then moved focus away from it).email.errors
: yet another property of theFormControl
object that contains the validation errors associated with the control.
The condition inside the *ngIf
directive is evaluated to true
if all the following conditions are met:
email
is truthy (i.e., it is not null, undefined, or false).email.dirty
is truthy (i.e., the control has been changed).email.touched
is truthy (i.e., the control has been blurred).email.errors
is truthy (i.e., there are validation errors associated with the control).
If the condition is true, the <ion-note>
element is rendered. Otherwise, it is not rendered. In the above code, the <ion-note>
element is used to display an error message when the email input is invalid, and it is shown only if the email input has been touched or dirty and has validation errors.
There were certain errors when implementing the above form input validation but ultimately got resolved when I added the AND operators and checked for all conditions simultaneously. The same logic applies to the password
input field too.
After creating the template file, figure 25, 26, 27, 28, 29, and 30 demonstrates the multiple use cases of the authentication along with figure 31 that demonstrates the user field updated in the firebase authentication. Ignore the social media logins for the moment because I will explain their implementation in the upcoming topics.
Figure 25: Login/Signup Page |
Figure 26: Invalid Credentials |
Figure 27: No existing user |
Figure 28: Registration failed |
Figure 29: Registration Success |
Figure 30: Login Success |
Figure 31: User authenticated and added to Users in Firebase console at the top. |
I forgot to mention this before, but in the firebase console in authentication, under Sign-IN method, it is important to add Email/Password as an authentication provider and enable it when the app is registered in firebase otherwise the authentication doesn’t work.
Next, I will create a logout button within the side menu in the app.component.html
like this:
<ion-footer>
<ion-list lines="none">
<ion-item class="ion-margin" (click)="logout()">
<ion-label>Logout</ion-label>
<ion-icon slot="end" name="log-out-outline"></ion-icon>
</ion-item>
...
</ion-list>
...
</ion-footer>
And create the method logout within the app.component.ts
in the following way:
...
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
standalone: true,
imports: [
...
],
})
export class AppComponent {
public environmentInjector = inject(EnvironmentInjector);
constructor(
private loadController: LoadingController,
private router: Router,
private authService: AuthService,
private toastCtrl: ToastController
) {}
// for the logout button on the menu page
async logout() {
// logout the user
const loading = await this.loadController.create({
// create a loading controller
message: 'Logging out...',
spinner: 'crescent', // spinner type
showBackdrop: true,
});
await loading.present(); // present the loading controller
await this.authService.logout(); // logout the user
await loading.dismiss(); // dismiss the loading controller
this.router.navigateByUrl('/tabs/login', { replaceUrl: true }); // navigate to the login page
const toast = await this.toastCtrl.create({
message: 'You have been logged out',
duration: 2000,
position: 'bottom',
});
toast.present();
}
}
The above code contains a logout
method that is triggered when the user clicks on the logout button on the menu page. First, it creates a LoadingController
with a message and a spinner type to indicate to the user that the application is logging out. It then presents the loading controller to the user using the present()
method. Next, it calls the logout
method of the AuthService
to log out the user. After the user is logged out, the loading controller is dismissed using the dismiss()
method. Then, the application navigates to the login page using the Router
and the navigateByUrl()
method. Finally, it creates a ToastController
to display a message to the user that they have been logged out. The toast message is then presented to the user using the present()
method.
And with this, I have set up firebase authentication in the application with appropriate authentication guards provided by firebase itself.
In this blog post, we have learned how to create a login page with firebase authentication using email provider and authentication guard in our Angular-15 Ionic-7 app. We have seen how to use AngularFireAuth service to sign up, sign in and sign out users, as well as how to protect our routes with canActivate method. We have also learned how to use Ionic components such as ion-input, ion-button and ion-alert to create a responsive and user-friendly interface.
We hope you have enjoyed this tutorial and found it useful. If you have any questions or feedback, please leave a comment below. You can also e-mail me to checkout out the source code of this project on GitHub. Thank you for reading and happy coding!