Day 6 : Improve signin/signup process and application workflow

Create a chat application with Ionic 5/ Angular 12/ Capacitor 3 and Django 3 – Series – Part six

Day 6 : Improve signin/signup process and application workflow

On previous tutorial, we learned how to register or signin to our application and made some modifications to pass the JWT Token to methods. Now we will learn how to improve the code and use Angular guards, to automatically add the jWT access token to our requests.

But first, we will remove the menu generated by Ionic template, which displays on the left side when navigating on web mode.

Let’s edit the src/app/app.component.html file to remove the ion-split-plane component and just keep the following code:

<ion-app>
    <ion-router-outlet id="main-content"></ion-router-outlet>
</ion-app>

Now we can edit the src/app/app.component.ts file and remove the variables appPages and label:

import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {

  constructor() {}
}

With these modifications, if you refresh your browser (http://localhost:8100/login-page), the login page or register page should take the full width of your browser with no menu on the left side.

Add JWT access token automatically with Angular Http interceptors

Angular provides a nice component called HttpInterceptor which intercepts any HttpRequest or HttpResponse to let us do some treatment before passing the request to the next usual process.

For example, we can edit the src/app/app.module.ts file and add the following code :

@Injectable()
export class AngularInterceptor implements HttpInterceptor {

  constructor(){
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let defaultTimeout = 10000;

      return next.handle(req).pipe(timeout(defaultTimeout),
        retryWhen(err=>{
          let retries = 1;
          return err.pipe(
            delay(500),
            map(error=>{
              if (retries++ ===3){
                throw error
              }
              return error;
            })
          )
        }),catchError(err=>{
          console.log(err)
          return EMPTY
        }), finalize(()=>{

        })
      )
    };    
}

To declare an HttpInterceptor and to use it we need to add the following code:

 { provide: HTTP_INTERCEPTORS, useClass: AngularInterceptor, multi: true } 

in our providers array list. Here is the full code of the src/app/app.module.ts file:

 import { Injectable, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { IonicStorageModule } from '@ionic/storage-angular';
import { Observable, EMPTY } from 'rxjs';
import { timeout, retryWhen, delay, map, catchError, finalize } from 'rxjs/operators';

@Injectable()
export class AngularInterceptor implements HttpInterceptor {

  constructor(){
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let defaultTimeout = 10000;

      return next.handle(req).pipe(timeout(defaultTimeout),
        retryWhen(err=>{
          let retries = 1;
          return err.pipe(
            delay(500),
            map(error=>{
              if (retries++ ===3){
                throw error
              }
              return error;
            })
          )
        }),catchError(err=>{
          console.log(err)
          return EMPTY
        }), finalize(()=>{

        })
      )
    };    
}

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    IonicStorageModule.forRoot(),
    BrowserModule, 
    HttpClientModule,
    IonicModule.forRoot(), 
    AppRoutingModule
  ],
  providers: [
    InAppBrowser,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: HTTP_INTERCEPTORS, useClass: AngularInterceptor, multi: true } 
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

This HttpInterceptor will be called at the start of every http request to add a default timeout of 10 secondes for the request, and if the request failed it will try again 3 times with 500ms between each request. Then after 3 times the request will failed.

It’s quite simple to understand the mecanism.

So now we will create a new file src/app/services/token.interceptor.ts file to manage our JWT token with the following code:

 import { UserManagerServiceService } from './user-manager-service.service';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { AuthenticationService } from './authentication.service';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  private refreshingInProgress: boolean;
  private accessTokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);

  constructor(private authService: AuthenticationService,
                private userManager:UserManagerServiceService,
              private router: Router) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const accessToken = this.authService.token;
    //console.log("=== Intercept token ")
    // console.log(accessToken)
    return next.handle(this.addAuthorizationHeader(req, accessToken)).pipe(
      catchError(err => {
        // in case of 401 http error
        console.log("====MODIF ERROR url:"+req.url)
        console.log("====STATUS:"+err.status)
        if (req.url.indexOf("/devices")>0){
          return throwError(err);
        }
        if (req.url.indexOf("/refresh")>0){
          //REFRESHTOKEN KO 
          console.log("==== 401 LOGOUT CAR ERROR")
          // otherwise logout and redirect to login page
          return this.logoutAndRedirect(err);
        }
        if (err instanceof HttpErrorResponse && err.status === 401) {
          // get refresh tokens

          console.log("===401 ask for a new TOKEN using the refresh token ============")
          const refreshToken = this.authService.refresh;

          // if there are tokens then send refresh token request
          if (refreshToken && accessToken) {
            return this.refreshToken(req, next);
          }
          console.log("No refresh or access token. Logout user ",req.url)
          // otherwise logout and redirect to login page
          return this.logoutAndRedirect(err);
        }

        // in case of 403 http error (refresh token failed)
        if (err instanceof HttpErrorResponse && err.status === 403) {
          // logout and redirect to login page
          console.log("Error 403 LOGOuT ",req.url)
          return this.logoutAndRedirect(err);
        }
        // if error has status neither 401 nor 403 then just return this error
        return throwError(err);
      })
    );
  }

  private addAuthorizationHeader(request: HttpRequest<any>, token: string): HttpRequest<any> {
    if (token) {
        let bearer = <code>Bearer ${token}</code>;
      ///  console.log("=== Clone request et add header avec token ",bearer)

      return request.clone({setHeaders: {Authorization: bearer}});
    }
    return request;
  }

  private logoutAndRedirect(err): Observable<HttpEvent<any>> {
    console.log("==== LOGOUT")
    this.userManager.logoutUser().then(()=>{

    })
    this.router.navigateByUrl('/login');
    return throwError(err);
  }

  private refreshToken(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.refreshingInProgress) {
      this.refreshingInProgress = true;
      this.accessTokenSubject.next(null);

      return this.authService.refreshToken().pipe(
        switchMap((res) => {
          this.refreshingInProgress = false;
          this.authService.token = res["access"]
          this.authService.refresh = res["refresh"]
          console.log("==== REFRESH SET VALUES")
          this.accessTokenSubject.next(res["access"]);
          // repeat failed request with new token
          return next.handle(this.addAuthorizationHeader(request, res["access"]));
        }), catchError((err, caught) => {
            console.log("=========== ERROR REFRESH TOKEN")
           // return throwError(err);
            return this.logoutAndRedirect(err);
          })
      );
    } else {
      // wait while getting new token
      return this.accessTokenSubject.pipe(
        filter(token => token !== null),
        take(1),
        switchMap(token => {
          // repeat failed request with new token
          return next.handle(this.addAuthorizationHeader(request, token));
        }));
    }
  }
}

and then edit our app.module.ts to use this new HttpInterceptor class:

  { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true } 

We can remove the HttpInterceptor added as example.

Full code should be now:

import { Injectable, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { IonicStorageModule } from '@ionic/storage-angular';
import { TokenInterceptor } from './services/token.interceptor';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    IonicStorageModule.forRoot(),
    BrowserModule, 
    HttpClientModule,
    IonicModule.forRoot(), 
    AppRoutingModule
  ],
  providers: [
    InAppBrowser,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true } 
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Ok it’s a lot of code. The TokenInterceptor class is quite complex (you can read more about it in this article, which is used and adapted to our Ionic application).

First our interceptor will automatically add the JWT token (that it gets from our AuthenticationService) to each request:

  const accessToken = this.authService.token;
  return next.handle(this.addAuthorizationHeader(req, accessToken))

using this method:

  private addAuthorizationHeader(request: HttpRequest<any>, token: string): HttpRequest<any> {
    if (token) {
        let bearer = <code>Bearer ${token}</code>;
      ///  console.log("=== Clone request et add header avec token ",bearer)

      return request.clone({setHeaders: {Authorization: bearer}});
    }
    return request;
  }

And then using a pipe will analyze the HttpResponse to check Http status code.

If the status code is 401 it means the token has expired and will try to refresh it using the JWT refresh token except if the status 401 is from the request with url path /refresh which means our JWT refresh token has expired too. In that case we have no choice to logout the user, to ask him again it’s credentials.

If the status code is 403, it means credentials are not valid anymore and again we have no choice to logout the user, to ask him again it’s credentials.

If you don’t understand the full code, it’s ok you can just used it as it is in your application. You just need to know it will manage JWT authentication for you.

Now we can modify the code to remove the accessToken parameters we passed to our User ApiService methods:

src/app/services/api-service.service.ts file :

  // Create a user 
  createUser(modelToCreate) {
    // model JSON
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };
    console.log("URL " + this.getUserUrl)
    return this.http.post(this.getUserUrl, modelToCreate, options).pipe(retry(1))
  }

  //Find user based on parameters
  findUserWithQuery(query) {
    let url = this.getUserUrl + query;
    return this.findUser(url)
  }

  private findUser(url) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };

    return Observable.create(observer => {
      this.http.get(url, options)
        .pipe(retry(1))
        .subscribe(res => {
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next();
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

  getUserDetails(id) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };
    return Observable.create(observer => {
      this.http.get(this.getUserUrl + id + "/", options)
        .pipe(retry(1))
        .subscribe(res => {
          this.networkConnected = true
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next(false);
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

  updateUser(id, patchParams) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };
    return Observable.create(observer => {
      this.http.patch(this.getUserUrl + id + "/", patchParams, options)
        .pipe(retry(1))
        .subscribe(res => {
          this.networkConnected = true
          observer.next(true);
          observer.complete();
        }, error => {
          observer.next(false);
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

  putUser(object) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'

      })
    };
    return Observable.create(observer => {
      this.http.put(this.getUserUrl + object.id + "/", object, options)
        .pipe(retry(1))
        .subscribe(res => {
          this.networkConnected = true
          observer.next(true);
          observer.complete();
        }, error => {
          observer.next(false);
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

  deleteUser(id) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };
    return this.http.delete(this.getUserUrl + id + "/", options).pipe(retry(1))
  }

}

and adapt our Login and Register pages, to remove the accessToken parameter from the call:

this.apiService.findUserWithQuery("?email="+email).subscribe((list) => {
this.apiService.updateUser(this.userManager.currentUser.id, updateParams).subscribe((done) => {

If you run again your Ionic application and try to login or register again, it should work which means our HttpInterceptor is working fine and added the token to our requests.

Auto login with Angular guards

Once registered or logged in, we would like to go to our main page named home. Let’s create it:

ionic g page HomePage
mv src/app/home-page src/app/pages 

and let’s modify the app.routing.module.ts file as we learned to reflect the path change:

,
  {
    path: 'home-page',
    loadChildren: () => import('./pages/home-page/home-page.module').then( m => m.HomePagePageModule)
  },

At this stage, our declared routes should be :

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login-page',
    pathMatch: 'full'
  },
  {
    path: 'register-page',
    loadChildren: () => import('./pages/register-page/register-page.module').then( m => m.RegisterPagePageModule)
  },
  {
    path: 'login-page',
    loadChildren: () => import('./pages/login-page/login-page.module').then( m => m.LoginPagePageModule)
  },
  {
    path: 'home-page',
    loadChildren: () => import('./pages/home-page/home-page.module').then( m => m.HomePagePageModule)
  },

];

login-page, register-page and home-page which correspond to the url /login-page/, /register-page/ and /home-page/.

To change the urls path and remove the -page, we could rewrite our routes to:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'register',
    loadChildren: () => import('./pages/register-page/register-page.module').then( m => m.RegisterPagePageModule)
  },
  {
    path: 'login',
    loadChildren: () => import('./pages/login-page/login-page.module').then( m => m.LoginPagePageModule)
  },
  {
    path: 'home',
    loadChildren: () => import('./pages/home-page/home-page.module').then( m => m.HomePagePageModule)
  },

];

Ok now as you should have notice, each time you will launch Ionic, you will be redirected to the Login page because of our default route :

 {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },

But if the user is already login or registered, we should go to the HomePage. To do that, we will use another Angular functionality which is Guards.

We will use the canLoad instruction. Let’s create a new directory and ask Ionic to create our class:

mkdir src/app/guards
ionic g guard guards/autoLogin --implements CanLoad

The class will be called AutoLoginGuard. Let’s replace the generated code with our own code implementation:

import { Injectable } from '@angular/core';
import { CanLoad, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthenticationService } from '../services/authentication.service';
import { filter, map, take } from 'rxjs/operators';
import { User } from '../models/user';
import { UserManagerServiceService } from 'src/app/services/user-manager-service.service';

@Injectable({
  providedIn: 'root'
})
export class AutoLoginGuard implements CanLoad {
  constructor(private authService: AuthenticationService, private router: Router,private userManager:UserManagerServiceService) {

   }

  canLoad(): Observable<boolean> {    
    return this.authService.isAuthenticated.pipe(
      filter(val => val !== null), // Filter out initial Behaviour subject value
      take(1), // Otherwise the Observable doesn't complete!
      map(isAuthenticated => {
        console.log('Found previous token ?, automatic login '+isAuthenticated);
        if (isAuthenticated) {
          //Load user 
          this.userManager.getUser().then((user:User)=>{
           if (user){
              this.router.navigateByUrl('/home', { replaceUrl: true });
            }
            else{
              console.log("=== No user")
              this.authService.isAuthenticated.next(false);
              this.router.navigateByUrl('/login', { replaceUrl: true });
              return true;
            }
          })
        } else {          
          return true;
        }
      })
    );
  }
}

In the canLoad method, we use our AuthenticationService to check if we are authenticated and if we can found a user, and if so we can redirect to our HomePage otherwise the user is not authenticated and will be redirected to the login page.

We just need to modify our routes, to add this canLoad instruction:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { AutoLoginGuard } from './guards/auto-login.guard';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'register',
    canLoad: [AutoLoginGuard],
    loadChildren: () => import('./pages/register-page/register-page.module').then( m => m.RegisterPagePageModule)
  },
  {
    path: 'login',
    canLoad: [AutoLoginGuard],
    loadChildren: () => import('./pages/login-page/login-page.module').then( m => m.LoginPagePageModule)
  },
  {
    path: 'home',
    loadChildren: () => import('./pages/home-page/home-page.module').then( m => m.HomePagePageModule)
  },

];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

And voila, if you run your Ionic application again

ionic serve

You should be redirected to the HomePage automatically (if you had register or login before).

Cleaning Ionic cache in the browser

To verify the behaviour is correct, we can cleaned our application cache using the developer tools of the browser:
CleanIonic

Just click the Clear site data button, and then refresh the page or go to the default Ionic url (http://localhost:8100/) and this time, you should see the LoginPage.

You could try to login again, and once again call the default ionic url (http://localhost:8100/), you should be redirected to the HomePage

Now that we have an HomePage we could modify our LoginPage and RegisterPage to redirect to this page using the router.navigateByUrl method:

// Next screen
console.log("===Can go to next screen")
this.router.navigateByUrl('/home', { replaceUrl: true });

Create and protect our application Home page

We just saw how to automatically redirect the user to the HomePage once authenticated. However we didn’t protect the HomePage from unathorized access. Once again clean your application cache as we learned previously, and just go to the url : http://localhost:8100/home.

The HomePage will appear even if the user is not registered or logged in (we just clean the cache remember.)

To avoid this behaviour and protect all the pages of our application, we will create another Angular Guard class named AuthGuard :

ionic g guard guards/auto --implements CanLoad

and modify our app-routing.module.ts to add the canLoad instruction to our HomePage:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { AutoLoginGuard } from './guards/auto-login.guard';
import { AutoGuard } from './guards/auto.guard';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'register',
    canLoad: [AutoLoginGuard],
    loadChildren: () => import('./pages/register-page/register-page.module').then( m => m.RegisterPagePageModule)
  },
  {
    path: 'login',
    canLoad: [AutoLoginGuard],
    loadChildren: () => import('./pages/login-page/login-page.module').then( m => m.LoginPagePageModule)
  },
  {
    path: 'home',
    canLoad:[AutoGuard],
    loadChildren: () => import('./pages/home-page/home-page.module').then( m => m.HomePagePageModule)
  },

];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Now we can edit our auto.guard.ts file to check if the user is authenticated or not:

import { AuthenticationService } from './../services/authentication.service';
import { Injectable, NgZone } from '@angular/core';
import { CanLoad, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
@Injectable({
  providedIn: 'root'
})
export class AutoGuard implements CanLoad {
  constructor(private authService: AuthenticationService,private router: Router) { }

  canLoad(): Observable<boolean> {    
    return this.authService.isAuthenticated.pipe(
      filter(val => val !== null), // Filter out initial Behaviour subject value
      take(1), // Otherwise the Observable doesn't complete!
      map(isAuthenticated => {
        if (isAuthenticated) {  
          return true;
        } else {          
          this.router.navigateByUrl('/login', { replaceUrl: true })
          return false;
        }
      })
    );
  }
}

This code will check if the user is authenticated and if not will redirect it to the LoginPage.

Try again to call the url http://localhost:8100/home and you will see the LoginPage redirection.

The source code for this tutorial is available on my GitHub repository.

Questions / Answers

  1. How to add custom code behaviour to HTTP request or response ?

    Use Angular Http Interceptor

  2. How to add specific code behaviour to be called before Ionic routes to urls ?

    Use Angular guards such as canLoad

Christophe Surbier