Building a JWT authentication flow is one of the basic things most apps have these days, but there are tricky elements that can make or break your app.
Today we will dive into the creation of an Ionic JWT app that allows us to login and protect our pages even when accessed as a URL in the browser. We also wanna have a tab bar routing, which is guarded by a login page upfront.
We’ll not create a backend but mock some data in the right place, but you can basically copy the whole approach from here into your project and just add your API URL in the right places.
Also, we’ll not make any authenticated requests in here and only focus on the autentication flow in our app. For more examples and courses on this topic check out the Ionic Academy.
Setting up our Ionic JWT Authentication App
First of all we start a new app but use the tabs template this time in order to save some time. If you don’t need tabs you can of course pick something else, most of the code would be the same.
We also need pages for our login and register which will be available to every user, and a service and guard that will hold all of our authentication logic.
Finally, the Ionic Storage package will be used to store our JWT and to decode it to retrieve its information. Now go ahead and run:
ionic start devdacticAuth tabs cd ./devdacticAuth ionic g page login ionic g page register ionic g service services/auth ionic g guard guards/auth npm i @ionic/storage @auth0/angular-jwt
In order to use our packages we also need to update our app/app.module.ts like this:
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 { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { IonicStorageModule } from '@ionic/storage'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [AppComponent], entryComponents: [], imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, IonicStorageModule.forRoot(), HttpClientModule], providers: [ StatusBar, SplashScreen, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } ], bootstrap: [AppComponent] }) export class AppModule {}
Now we are ready to store our token and make some dummy HTTP requests, let’s move on!
Changing the App Routing
The routing was always a huge problem (as I saw in many comments), so let’s make this as easy as possible.
We basically have three top level paths:
- ”: Empty path, called in the beginning, displays the login page
- ‘register’: Opens the registration page
- ‘members’: Changed from ‘tabs’ to this to make the URL more pretty, basically the parent path of the tabs layout. The tabs actually have their own routing specified in another routing file in the tabs/ folder, we’ll get to that.
You can also already see that we apply our AuthGuard to the members path, which means everyone who wants to access a path starting with “members/” needs to be authenticated. So it can only be activated once the guard allows it, and we’ll implement that functionality soon.
In the code below we also use the new Angular 8 syntax for importing our pages, so go ahead and change your app/app-routing.module.ts to:
import { NgModule } from '@angular/core'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './guards/auth.guard'; const routes: Routes = [ { path: 'members', loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule), canActivate: [AuthGuard] }, { path: '', loadChildren: () => import('./login/login.module').then(m => m.LoginPageModule) }, { path: 'register', loadChildren: () => import('./register/register.module').then(m => m.RegisterPageModule) } ]; @NgModule({ imports: [ RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) ], exports: [RouterModule] }) export class AppRoutingModule {}
As I said we also made a tiny change to the tabs routing, especially the names. This is just to show how you could change the elements in order to have a more polished URL, and automatically redirect to the first tab if we come here without a tab being selected (for example when someone would navigate only to “members/”.
For this, open your app/tabs/tabs.router.module and change it to:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { TabsPage } from './tabs.page'; const routes: Routes = [ { path: '', redirectTo: '/members/tab1', pathMatch: 'full' }, { path: '', component: TabsPage, children: [ { path: 'tab1', children: [ { path: '', loadChildren: () => import('../tab1/tab1.module').then(m => m.Tab1PageModule) } ] }, { path: 'tab2', children: [ { path: '', loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule) } ] }, { path: 'tab3', children: [ { path: '', loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule) } ] } ] } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class TabsPageRoutingModule {}
Now we got the routing in place and you should be able to see the login, and if you navigate to http://localhost:8100/members/tab1 you should also still see your tab bar interface!
Now we just need to connect all of this with some logic.
Building the JWT Authentication Logic
The JWT authentication flow is the most important part in here, so we start with our service.
Let’s take a look at the different functions in detail:
loadStoredToken()
This function is meant to check your storage for a previously saved JWT. If we can find it, we decode the information and return that the user is already authenticated. All of this is wrapped into the user
Observable.
We do this in order to use an Observable in the canActivate
function of our guard in the next step.
The problem with previous examples was that initially a user is null when checking for it too early, which resulted in a flash of the login page and then showing the logged in pages.
We have to be careful to wait for the platform to be ready, then grab the token from storage. Both of these functions return a Promise, but with the help of the from() function we can convert them to Observables and easily use them in our pipe block.
PS: Using switchMap
inside the pipe basically switches from one Observable to another!
login()
In here you would normally make a POST call to your backend and retrieve a JWT, but as I wanted to keep things simple we make a random API call and then simply change the result to a token I generated online.
We can then decode the token again and write it to the storage. Keep in mind that this write operation also returns a Promise, so we convert it to an Observable that we then return.
getUser()
This function will simply return the current value of our BehaviourSubject
– the place where we store the decoded JWT data!
logout()
On logout we remove the JWT from storage, set the userData
to null and then use the Router to guide the user back to the login page.
All of this takes now place right inside our services/auth.service.ts:
import { Platform } from '@ionic/angular'; import { Injectable } from '@angular/core'; import { Storage } from '@ionic/storage'; import { BehaviorSubject, Observable, from, of } from 'rxjs'; import { take, map, switchMap } from 'rxjs/operators'; import { JwtHelperService } from "@auth0/angular-jwt"; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; const helper = new JwtHelperService(); const TOKEN_KEY = 'jwt-token'; @Injectable({ providedIn: 'root' }) export class AuthService { public user: Observable<any>; private userData = new BehaviorSubject(null); constructor(private storage: Storage, private http: HttpClient, private plt: Platform, private router: Router) { this.loadStoredToken(); } loadStoredToken() { let platformObs = from(this.plt.ready()); this.user = platformObs.pipe( switchMap(() => { return from(this.storage.get(TOKEN_KEY)); }), map(token => { if (token) { let decoded = helper.decodeToken(token); this.userData.next(decoded); return true; } else { return null; } }) ); } login(credentials: {email: string, pw: string }) { // Normally make a POST request to your APi with your login credentials if (credentials.email != 'saimon@devdactic.com' || credentials.pw != '123') { return of(null); } return this.http.get('https://randomuser.me/api/').pipe( take(1), map(res => { // Extract the JWT, here we just fake it return `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1Njc2NjU3MDYsImV4cCI6MTU5OTIwMTcwNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDUiLCJmaXJzdF9uYW1lIjoiU2ltb24iLCJsYXN0X25hbWUiOiJHcmltbSIsImVtYWlsIjoic2FpbW9uQGRldmRhY3RpYy5jb20ifQ.4LZTaUxsX2oXpWN6nrSScFXeBNZVEyuPxcOkbbDVZ5U`; }), switchMap(token => { let decoded = helper.decodeToken(token); this.userData.next(decoded); let storageObs = from(this.storage.set(TOKEN_KEY, token)); return storageObs; }) ); } getUser() { return this.userData.getValue(); } logout() { this.storage.remove(TOKEN_KEY).then(() => { this.router.navigateByUrl('/'); this.userData.next(null); }); } }
With the service in place we can use its functionality in our guard to check if a user is authenticated and therefore allowed to access a protected page.
As said before this function can actually return a lot of different things, one being an Observable. On this way we can truly wait for all the platform and storage stuff and make sure we return if we found a user token or not.
If the user is not allowed we will present an alert and guide him back to the login, otherwise we can return true
which signals the router that the page should be displayed.
Go ahead with your guards/auth.guard.ts and change it to:
import { AuthService } from './../services/auth.service'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { take, map } from 'rxjs/operators'; import { AlertController } from '@ionic/angular'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate{ constructor(private router: Router, private auth: AuthService, private alertCtrl: AlertController) { } canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { return this.auth.user.pipe( take(1), map(user => { if (!user) { this.alertCtrl.create({ header: 'Unauthorized', message: 'You are not allowed to access that page.', buttons: ['OK'] }).then(alert => alert.present()); this.router.navigateByUrl('/'); return false; } else { return true; } }) ) } }
Ok that’s enough of RxJS logic for one tutorial, things get a lot easier from now on, I promise (pun intended).
Creating the Public & Protected App Pages
The login page needs just two buttons and two input fields for now and is fairly simple compared to everything before, so simple go ahead and change your app/login/login.page.html to:
<ion-header> <ion-toolbar color="primary"> <ion-title>Devdactic Auth</ion-title> </ion-toolbar> </ion-header> <ion-content class="ion-padding"> <ion-row> <ion-col size="12" size-sm="10" offset-sm="1" size-md="8" offset-md="2" size-lg="6" offset-lg="3" size-xl="4" offset-xl="4"> <ion-card> <ion-card-header> <ion-card-title class="ion-text-center">Your Account</ion-card-title> </ion-card-header> <ion-card-content> <ion-item lines="none"> <ion-label position="stacked">Email</ion-label> <ion-input type="email" placeholder="Email" name="email" [(ngModel)]="credentials.email"></ion-input> </ion-item> <ion-item lines="none"> <ion-label position="stacked">Password</ion-label> <ion-input type="password" placeholder="Password" name="password" [(ngModel)]="credentials.pw"> </ion-input> </ion-item> <ion-button (click)="login()" expand="block">Login</ion-button> <ion-button expand="block" color="secondary" routerLink="/register" routerDirection="forward">Register </ion-button> </ion-card-content> </ion-card> </ion-col> </ion-row> </ion-content>
Nothing fancy, right?
Going to the register page already works through the routerLink
we specified.
For the login we only need to call the function of our service and wait for the result – if it’s successful we route to the members path and otherwise simply display an alert!
There’s nothing else we need to do at this point as everything is happening in our service, so simply change your app/login/login.page.ts to:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../services/auth.service'; import { AlertController } from '@ionic/angular'; import { Router } from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.page.html', styleUrls: ['./login.page.scss'] }) export class LoginPage implements OnInit { credentials = { email: 'saimon@devdactic.com', pw: '123' }; constructor( private auth: AuthService, private router: Router, private alertCtrl: AlertController ) {} ngOnInit() {} login() { this.auth.login(this.credentials).subscribe(async res => { if (res) { this.router.navigateByUrl('/members'); } else { const alert = await this.alertCtrl.create({ header: 'Login Failed', message: 'Wrong credentials.', buttons: ['OK'] }); await alert.present(); } }); } }
As a quick note, you also need to add a back button to the register page if you don’t want to get stuck on it. But for now we won’t implement all the register fields and logic – if you want more guidance and material on this just check out the Ionic Academy!
Therefore if you want to change your app/register/register.page.html to this:
<ion-header> <ion-toolbar color="secondary"> <ion-buttons slot="start"> <ion-back-button defaultHref="/"></ion-back-button> </ion-buttons> <ion-title>Register</ion-title> </ion-toolbar> </ion-header> <ion-content padding> </ion-content>
Now your JWT login is working. THAT’S A SUCCESS!
But in order to show some data and complete the routing, there’s one more thing we need to do.
Showing User Data based on the JWT
We already decode the JWT information in our service, so let’s also make use of that data!
Because the service is extracting all the data before we even enter a secured page we can access it pretty safely now. We simply call the getUser()
function which will return some data. And this is not an async operation because the service function is simply getting the current value of the userData
BehaviourSubject!
Therefore, go ahead and change the app/tab2/tab2.page.ts to:
import { Component } from '@angular/core'; import { AuthService } from '../services/auth.service'; @Component({ selector: 'app-tab2', templateUrl: 'tab2.page.html', styleUrls: ['tab2.page.scss'] }) export class Tab2Page { user = null; constructor(private auth: AuthService) {} ionViewWillEnter() { this.user = this.auth.getUser(); } logout() { this.auth.logout(); } }
Also, the logout basically only happens in our service, we just need to call the right functionality.
Now the last part is to show some information from our decoded JWT and allow the user to logout in order to close the circle of our JWT authentication flow.
To do so, simply apply the following to your app/tab2/tab2.page.html:
<ion-header> <ion-toolbar color="primary"> <ion-title> Tab Two </ion-title> </ion-toolbar> </ion-header> <ion-content> <ion-card *ngIf="user"> <ion-card-content> {{ user.email }} - {{ user.first_name }} {{ user.last_name }} </ion-card-content> </ion-card> <ion-button expand="block" (click)="logout()">Logout</ion-button> </ion-content>
And that’s all we had to do to build a very robust JWT authentication for a tab based Ionic app!
Conclusion
If you understand the mechanism and respect how Promises and Observables work, it’s quite easy to chain them together.
Having race conditions of any kind in your app is no good idea, so always make sure there’s no possibility things can go wrong by chaining them accordingly.
Now this app isn’t finished, you need to add your own API call to get a JWT, but then you could fully integrate the JWT package we used to automatically sign all the future requests of your app with the stored JWT, something we also implemented before in this tutorial.
You can also find a video version of this tutorial below.
The post Building an Ionic 4 JWT Login with Tab Bar & Angular Routing appeared first on Devdactic.