The following is a custom example and tutorial on how to implement a working authentication example with Ionic 4.
 
As this is a complex tutorial I will change the usual format of my articles. Instead of following the process with small code snippets, I will combine the whole code examples with underlying code commentary, in the hope it will make it easier to understand. To make your and my life even easier, I will also reuse my previous tutorials, first and foremost Ionic 4 | How To Create And Validate Forms.
 
We will not use the real backend API as this is just an example application; we will mock it instead. To use actual backend API just comment down dummyBackendProvider in /src/app/app.module.ts file.
 
 
 

Note: If this tutorial was helpful, you need further clarification, something is not working or you have a request for another Ionic post? Furthermore, leave me a comment below if you don't like something about this blog, if something is bugging you, don't like how I'm doing stuff here. Feel free to comment below, subscribe to my blog, mail me to dragan.gaic@gmail.com. Thanks and have a nice day!
 
 

Related

 
 

Preparations

 
As in all my other tutorials on this topic, you need to have a working Ionic 4 workspace.
 
You will need these to finish your tutorial:
 
  • Android or iOS environment (optional as we can also run it in our browser)
  • NodeJS
  • Ionic
  • Cordova
 
If you neet to set-up a working Ionic 4 environment just follow this tutorial: Ionic [2|3] | Installation Guide.
 

1. Update Ionic CLI

 
Install or update your current Ionic version:
 
npm install -g ionic cordova
 
To update:
 
npm update -g ionic cordova
 
 

2. Create A New Project

 
ionic start IonicAuthenticationExample blank
cd IonicAuthenticationExample
 
You will find a working example at the end of this article, or keep reading if you are interested in how it works. Jump to the working example here.
 
Warning: As some of you don't have a prior Ionic CLI experience, from this point and on, every time I tell you to execute something, do that inside an example project folder.
 

3. Add Required Platform

 
If you want to use the browser:
 
ionic cordova platform add browser
 
Android platform:
 
ionic cordova platform add android
 
iOS platform:
 
ionic cordova platform add ios
 

Code Overview

 
The folder structure below presents our active project. By clicking on the blue link you will immediately jump on that specific file.
 

Folder Structure

 
 
 

Authentication Page

 
I have reused authentication page from my previous article on form validation topic (you will find the link in the opening words). As such, I will not go into great details with this part of the implementation. If you would like to know more just follow the above tutorial link.
 
We are using a very simple authentication page, two fields only, email and password. The only difference compared to my previous example is that on form submit we will trigger the actual login process.
 
The authentication process is handled in the authService.
 
To make this example as simple we are using a dummy backend provider that intercepts the HTTP requests and send back fake responses. More about it in the next chapter.
 
auth.page.html
 
<ion-header>
  <ion-toolbar>
    <ion-title>Authenticate</ion-title>
  </ion-toolbar>
</ion-header>
 
<ion-content padding="true">
    <form [formGroup]="authForm" (ngSubmit)="onSubmit(authForm.value)">
        <ion-item>
            <ion-label>Username (Email)</ion-label>
            <ion-input formControlName="email"></ion-input>
        </ion-item>
 
        <div *ngIf="submitted && frm.email.errors" class="invalid-feedback">
            <ion-item *ngIf="frm.email.errors.required">
                <p style="color:red">Email is required!</p>
            </ion-item>
            <ion-item *ngIf="frm.email.errors.email">
                <p style="color:red">Email must be a valid email address!</p>
            </ion-item>
        </div>        
 
        <ion-item>
            <ion-label>Password</ion-label>
            <ion-input formControlName="password" type="password"></ion-input>
        </ion-item>
 
        <div *ngIf="submitted && frm.password.errors" class="invalid-feedback">
            <ion-item *ngIf="frm.password.errors.required">
                <p style="color:red">Password is required!</p>
            </ion-item>
            <ion-item *ngIf="frm.password.errors.minlength">
                <p style="color:red">Password must be at least 6 characters!</p>
            </ion-item>
        </div>
 
        <ion-grid>
            <ion-row>
                <ion-col>
                    <ion-button expand="block" size="large" type="submit">Authenticate  <ion-spinner name="bubbles" *ngIf="loading"></ion-spinner></ion-button>    
                </ion-col>
                <ion-col>
                    <ion-button expand="block" size="large" type="reset" (click)="onReset()">Reset</ion-button> 
                </ion-col>
            </ion-row>
        </ion-grid>
    </form>
</ion-content>
 
auth.page.ts
 
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AlertController } from '@ionic/angular';

import { AuthService } from '../../services/auth.service';
 
@Component({
  selector: 'app-auth',
  templateUrl: './auth.page.html',
  styleUrls: ['./auth.page.scss'],
})
export class AuthPage implements OnInit {
 
    submitted = false;
    authForm: FormGroup;
    returnUrl: string;
    loading = false;
 
    constructor(private router: Router,
                private route: ActivatedRoute, 
    	        private formBuilder: FormBuilder,
    	        private authService: AuthService,
    	        private alertController: AlertController) { 
		this.router.navigate(['/']);
    }
 
    ngOnInit() {
        this.authForm = this.formBuilder.group({
            email: ['dragan.gaic@gmail.com', [Validators.required, Validators.email]],
            password: ['passw0rd', [Validators.required, Validators.minLength(6)]]
        }, {});

        this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';        
    }
 
    onSubmit(value: any): void { 
        this.submitted = true;
 
        // Stop if the form validation has failed
        if (this.authForm.invalid) {
            return;
        }
 
        this.loading = true;
        this.authService.login(this.frm.email.value, this.frm.password.value)
            .pipe(first())
            .subscribe(
                data => {
                    this.loading = false;
                    this.router.navigate([this.returnUrl]);
                },
                error => {
                    this.presentAlert(error.error.message);
                    this.loading = false;
                });        
    }

    onReset() {
        this.submitted = false;
        this.authForm.reset();
    }  

    async presentAlert(msg) {
	const alert = await this.alertController.create({
	  header: 'Alert',
	  subHeader: '',
	  message: msg,
	  buttons: ['OK']
	});

	await alert.present();
    }
 
    get frm() { return this.authForm.controls; }    
}
 

Authentication Service

 
This is a simple service that handles login and logout tasks and provides the currently active user.
 
The login function sends a POST REST request to the preconfigured backend API. Environment constants are preconfigured in the environment.ts class, just outside of the app folder. If the authentication was successful, we will encode username and password with BASE64, store it in localstorage and use for authentication header.
 
The logout function will remove user locastorage object, remove it as current logged in user and send us back to the authentication page.
 
Finaly, currentUserValue function will return the current active user. We will use it to display loged in user in the Home page.
 
auth.service.ts
 
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';

import { User } from '../models/user';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
    private currentUserSubject: BehaviorSubject<User>;
    public currentUser: Observable<User>;

    constructor(private http: HttpClient, private router: Router) {
        this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
        this.currentUser = this.currentUserSubject.asObservable();
    }

    public get currentUserValue(): User {
        return this.currentUserSubject.value;
    }

    public login(email: string, password: string) {
        return this.http.post<any>(`${environment.bffUrl}/users/authenticate`, { email, password })
            .pipe(map(user => {
                user.authdata = window.btoa(email + ':' + password);
                localStorage.setItem('currentUser', JSON.stringify(user));
                this.currentUserSubject.next(user);
                return user;
            }));
    }

    public logout() {
        localStorage.removeItem('currentUser');
        this.currentUserSubject.next(null);
        this.router.navigate(['/auth']);
    }
}

 

User Model

 
The user model is a simple class describing the application User object.
 
user.ts
 
export class User {
    id: number;
    email: string;
    password: string;
    firstName: string;
    lastName: string;
    authdata?: string;
}
 

Dummy Backend Provider

 
Dummy backend provider is used to fake backend real backend API. It is based on Angular HttpInterceptor, and as its name states it intercepts HTTP requests and send back fake responses.
 
To make it fell a bit more realistic, for each login related route, we are faking 0,5-sec response delay.
 
handleRoute function is used for actual interception. It will trigger only if requests match on of these two route patterns /users/authenticate and /users and their related request types (POST and GET).
 
Authentication process will trigger authenticate function. This is the location where actual faking is taking part. The list of users is already provided in the constant users, and we will match it against the incoming authentication form information. The function will return ok if the authentication result was successful, or error in the case of failure.
 
Another important part of this dummy interceptor is getUsers function. While it has no real purpose in this example it is put there to showcase how to handle specific tasks only if the user was successfully authenticated and the correct auth header is available. Of course, this is something you will need to implement on your backend.
 
To use the real backend provider just comment dummyBackendProvider in app.module.ts file.
 
dummy.backend.interceptor.ts
 
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';

import { User } from '../models/user';

const users: User[] = [{ id: 1, email: 'dragan.gaic@gmail.com', password: 'passw0rd', firstName: 'Dragan', lastName: 'Gaic' }];

@Injectable()
export class DummyBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        return of(null)
            .pipe(mergeMap(handleRoute))
            .pipe(materialize())
            .pipe(delay(500))
            .pipe(dematerialize());

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                default:
                    return next.handle(request);
            }    
        }

        function authenticate() {
            const { email, password } = body;
            const user = users.find(x => x.email === email && x.password === password);
            if (!user) return error('Email or password is incorrect');
            return ok({
                id: user.id,
                email: user.email,
                firstName: user.firstName,
                lastName: user.lastName
            })
        }

        function getUsers() {
            if (!isLoggedIn()) return unauthorized();
            return ok(users);
        }

        function ok(body?) {
            return of(new HttpResponse({ status: 200, body }))
        }

        function error(message) {
            return throwError({ error: { message } });
        }

        function unauthorized() {
            return throwError({ status: 401, error: { message: 'Unauthorised' } });
        }

        function isLoggedIn() {
            return headers.get('Authorization') === `Basic ${window.btoa('test:test')}`;
        }
    }
}

export let dummyBackendProvider = {
    provide: HTTP_INTERCEPTORS,
    useClass: DummyBackendInterceptor,
    multi: true
};
 
app.module.ts
 
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

import { dummyBackendProvider } from '../app/interceptors/dummy.backend.interceptor';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,

    // Remove this line if you want to use the real BFF API
    dummyBackendProvider,
    
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
 

Routing Interceptor

 
Routing interceptor is probably the most important part of this implementation. It will intercept application route changes to determine if the user is allowed to access sections of our application. For example, if the user was not authenticated this interceptor will prevent it from accessing restricted routes.
 
To make it work we are using Angular CanActivate interface. Interceptor will be able to use canActivate() function to decide if a route can be activated or not. It will return true if we are allowed or false if we are not allowed.
 
To check if the user is allowed we are using authService object currentUserValue.
 
routing.interceptor.ts
 
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthService } from '../services/auth.service';

@Injectable({ providedIn: 'root' })
export class RoutingInterceptior implements CanActivate {
    constructor(
        private router: Router,
        private authService: AuthService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUser = this.authService.currentUserValue;
        if (currentUser) {
            return true;
        }

        this.router.navigate(['/auth'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}
 

Error and Authentication Interceptor

 
Above interceptors also serve a very important role. Error interceptor will intercept all backend error responses and logout user if the 401 Unauthorized response was detected. On the other hand, authentication interceptor will intercept all HTTP requests and create an authentication header if the active user was detected and if it holder BASE64 representation of username and password. Authentication Interceptor is not important in our example as it is not used, however, you will need this kind of logic to authorize the use of other backend API calls.
 
error.interceptor.ts
 
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthService } from '../services/auth.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authService: AuthService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                this.authService.logout();
                location.reload(true);
            }

            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}
 
authentication.interceptor.ts
 
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { AuthService } from '../services/auth.service';

@Injectable()
export class BasicAuthInterceptor implements HttpInterceptor {
    constructor(private authService: AuthService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add authorization header with basic auth credentials if available
        const currentUser = this.authService.currentUserValue;
        if (currentUser && currentUser.authdata) {
            request = request.clone({
                setHeaders: { 
                    Authorization: `Basic ${currentUser.authdata}`
                }
            });
        }

        return next.handle(request);
    }
}
 
Continue to the next page