JWT Authentication with Ionic & Node.js - Part 2: The Ionic App Last update: 2018-11-27
Authentication for Ionic apps is mandatory in a great amount of apps so we can’t talk enough about the topic. Also, this approach works almost the same for pure Angular apps without Ionic so it’s definitely something you should know about!
We are already at the second part where we will develop the actual Ionic app. Inside the first part we have built the server for our JSON Web Token authentication so make sure you got that server up and running by now!
- Part 1: The Node.js Server
- Part 2: The Ionic App - You are here!
Once we are done with this part you have a fully working authentication system working where users can signup, register and login to pages that only logged in users can see!
Starting our JWT Auth Ionic App
We start our Ionic app like always with a blank project and add a few pages and services that we will need for the authentication. Also, we install the Ionic storage package where we will store the JWT (for pure Angular we would simply use localstorage) and finally add another helping package called angular-jwt, so go ahead and run:
ionic start devdacticAuth blank --type=angular
cd devdacticAuth
ionic g page pages/login
ionic g page pages/inside
ionic g service services/auth
ionic g service services/authGuard
npm i @ionic/storage
npm i @auth0/angular-jwt
ionic cordova plugin add cordova-sqlite-storage
Once your app is ready we need to configure our JWT package for our module: We need to let it know where our access token is stored and also since a later version we have to supply an array of whitelistedDomains inside the jwtOptionsFactory
.
The trick is: For the domains listed in that array the package will automatically add the “Authorization: Bearer xyz” header with our token to every request of the Angular HttpClient!
Besides that we also need to include the HttpClientModule
inside our module and the Ionic Storage, so change your app/app.module.ts to:
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 { Storage, IonicStorageModule } from '@ionic/storage';
import { JwtModule, JWT_OPTIONS } from '@auth0/angular-jwt';
export function jwtOptionsFactory(storage) {
return {
tokenGetter: () => {
return storage.get('access_token');
},
whitelistedDomains: ['localhost:5000']
};
}
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
HttpClientModule,
IonicStorageModule.forRoot(),
JwtModule.forRoot({
jwtOptionsProvider: {
provide: JWT_OPTIONS,
useFactory: jwtOptionsFactory,
deps: [Storage]
}
})
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
Since Ionic 4 is a lot closer to Angular we can now also store our environment variables in a file so we can autopmatically switch between dev and prod variables. It’s not really needed in our tutorial but still we want to follow a good coding style so add the URL of your server to the src/environments/environment.ts:
export const environment = {
production: false,
url: 'http://localhost:5000'
};
In our case the server should run exactly there, it’s also the whitelisted domain we passed to the JWT helper. If you have deployed the server or run it on a different port make sure to change those values.
Creating the Authentication Logic
We start with the core functionality of our app by implementing the authentication service. There are quite a few functions but I don’t want to split up the code so here’s the explanation for the whole service that follows afterwards:
- We have an
authenticationState
Behaviour Subject which is a special type of Observable where we can emit new values to all subscribers - Inside the constructor we always check for an existing token so we can automatically log in a user
- checkToken looks up our storage for a valid JWT and if found, changes our
authenticationState
s - Register and login do exactly what they sound like and also upon successful login we will decode the token and save it to our storage so it can be used for our authenticated requests later
- Logout reverts the login and clears the token from the storage plus changes our current authentication state
- The special data route is the only protected route inside our API so the call only works once we are authenticated. Also, if we get back an unauthorised error our token seems to be invalid so we can throw an error and change our auth state again
- To get the current value of our authentication state we can call the
isAuthenticated
function. Subjects are pretty cool!
Ok that’s the idea behind our service, of course it’s not enough to make our app secure but for now go ahead and change your app/services/auth.service.ts to this:
import { Platform, AlertController } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { JwtHelperService } from '@auth0/angular-jwt';
import { Storage } from '@ionic/storage';
import { environment } from '../../environments/environment';
import { tap, catchError } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
const TOKEN_KEY = 'access_token';
@Injectable({
providedIn: 'root'
})
export class AuthService {
url = environment.url;
user = null;
authenticationState = new BehaviorSubject(false);
constructor(
private http: HttpClient,
private helper: JwtHelperService,
private storage: Storage,
private plt: Platform,
private alertController: AlertController
) {
this.plt.ready().then(() => {
this.checkToken();
});
}
checkToken() {
this.storage.get(TOKEN_KEY).then((token) => {
if (token) {
let decoded = this.helper.decodeToken(token);
let isExpired = this.helper.isTokenExpired(token);
if (!isExpired) {
this.user = decoded;
this.authenticationState.next(true);
} else {
this.storage.remove(TOKEN_KEY);
}
}
});
}
register(credentials) {
return this.http.post(`${this.url}/api/register`, credentials).pipe(
catchError((e) => {
this.showAlert(e.error.msg);
throw new Error(e);
})
);
}
login(credentials) {
return this.http.post(`${this.url}/api/login`, credentials).pipe(
tap((res) => {
this.storage.set(TOKEN_KEY, res['token']);
this.user = this.helper.decodeToken(res['token']);
this.authenticationState.next(true);
}),
catchError((e) => {
this.showAlert(e.error.msg);
throw new Error(e);
})
);
}
logout() {
this.storage.remove(TOKEN_KEY).then(() => {
this.authenticationState.next(false);
});
}
getSpecialData() {
return this.http.get(`${this.url}/api/special`).pipe(
catchError((e) => {
let status = e.status;
if (status === 401) {
this.showAlert('You are not authorized for this!');
this.logout();
}
throw new Error(e);
})
);
}
isAuthenticated() {
return this.authenticationState.value;
}
showAlert(msg) {
let alert = this.alertController.create({
message: msg,
header: 'Error',
buttons: ['OK']
});
alert.then((alert) => alert.present());
}
}
It’s not yet enough because if you want to make your app more secure you have to prevent access to certain pages if the users are not logged in.
To do so we have already created a new Route Guard with the CLI in the beginning in which we can implement the canActivate
function.
Inside that function we can now simply check for the current state of our authentication and later apply this guard to the routes that we want to protect! Simply open your app/services/auth-guard.service.ts and change it to:
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(public auth: AuthService) {}
canActivate(): boolean {
return this.auth.isAuthenticated();
}
}
Route Guards & Automatic Routing
Because we use Ionic 4 we have to talk about routing. To navigate through our app we have to define the different paths and components for them and also we want to apply the guard from before to the inside page of course.
In our case we need to wire up the login page and the inside page accordingly so open your general routing at app/app-routing.module.ts and change it to:
import { AuthGuardService } from './services/auth-guard.service';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', loadChildren: './pages/login/login.module#LoginPageModule' },
{
path: 'inside',
loadChildren: './pages/inside/inside.module#InsidePageModule',
canActivate: [AuthGuardService]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
Now our inside route is protected using the guard and whenever a user tries to navigate to that URL, the check will be called and access is prevented if he’s not authenticated!
This is a basic example, and you can also find examples for creating a tab bar or building a side menu on the Ionic Academy for free!
To make everything a bit more comfortable for the user we can subscribe to the authentication state at the top level of our app so we immediately notice any changes and react accordingly.
This means, once we are authenticated we can automatically navigate to the inside page and for the opposite throw the user back to the login page. To do so simply change your app/app.component.ts top include the subscription check:
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AuthService } from './services/auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private authService: AuthService,
private router: Router
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
this.authService.authenticationState.subscribe((state) => {
if (state) {
this.router.navigate(['inside']);
} else {
this.router.navigate(['login']);
}
});
});
}
}
Alright, all services and logic is in place, now we just need to implement the 2 pages of our app and we are done!
Building the Login
To keep things simple our login is also the register page so we can submit the same form for different actions. Because we want to use a Reactive Form we need to make sure to include it inside the module of the page, so in our case add it to the app/pages/login/login.module.ts like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { LoginPage } from './login.page';
const routes: Routes = [
{
path: '',
component: LoginPage
}
];
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ReactiveFormsModule,
RouterModule.forChild(routes)
],
declarations: [LoginPage]
})
export class LoginPageModule {}
Inside our page we define our form to consist of the email and password, exactly what our server from the first part expects!
Because we have implemented the important functionality in our service the onSubmit
function that is called for the login only needs to call our service and wait. Remember, inside the service we sate the state after a successful login, and our app component at the top will notice the change and perform the needed routing for us!
Same counts for the register function but here we can also automatically call the login so when the user registers he will also be logged in directly afterwards which is quite common these days.
Simply change your app/pages/login/login.page.ts to this now:
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss']
})
export class LoginPage implements OnInit {
credentialsForm: FormGroup;
constructor(private formBuilder: FormBuilder, private authService: AuthService) {}
ngOnInit() {
this.credentialsForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
onSubmit() {
this.authService.login(this.credentialsForm.value).subscribe();
}
register() {
this.authService.register(this.credentialsForm.value).subscribe((res) => {
// Call Login to automatically login the new user
this.authService.login(this.credentialsForm.value).subscribe();
});
}
}
Our view is not really spectacular, we simply need to wire up our input fields with the appropriate names from our reactive form and add a check to our buttons if the form values are actually all valid. The rules for this check have been added to the form in the step before!
Also, if you want to have another button inside your form that will not trigger the form action, simply add type="button"
to have a standard button with it’s own click handler.
Open your app/pages/login/login.page.html and change it to:
<ion-header>
<ion-toolbar color="primary">
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<form [formGroup]="credentialsForm" (ngSubmit)="onSubmit()">
<ion-item>
<ion-label position="floating">Email</ion-label>
<ion-input type="email" formControlName="email"></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Password</ion-label>
<ion-input type="password" formControlName="password"></ion-input>
</ion-item>
<ion-button expand="full" type="submit" [disabled]="!credentialsForm.valid">Login</ion-button>
<ion-button expand="full" type="button" (click)="register()" [disabled]="!credentialsForm.valid"
>Register</ion-button
>
</form>
</ion-content>
Alright, now you can already fire up your app and see the login but we need one more thing.
Adding the Inside Page
To see if our whole authentication system actually works we need a page that is only available to users that are logged in.
On this inside page we only perform some dummy operations but they’ll help us to further understand the JWT auth concept. So our page can load the special information (adding auth headers is handled by the service/package automatically) that we have defined inside our server as a route that can only be accessed with a valid token.
The logout is pretty self explanatory, but we have another dummy function in which we can forcefully remove our JWT from the storage to see what happens afterwards. So go ahead and change your app/pages/inside/inside.page.ts to:
import { AuthService } from './../../services/auth.service';
import { Component, OnInit } from '@angular/core';
import { Storage } from '@ionic/storage';
import { ToastController } from '@ionic/angular';
@Component({
selector: 'app-inside',
templateUrl: './inside.page.html',
styleUrls: ['./inside.page.scss']
})
export class InsidePage implements OnInit {
data = '';
constructor(
private authService: AuthService,
private storage: Storage,
private toastController: ToastController
) {}
ngOnInit() {}
loadSpecialInfo() {
this.authService.getSpecialData().subscribe((res) => {
this.data = res['msg'];
});
}
logout() {
this.authService.logout();
}
clearToken() {
// ONLY FOR TESTING!
this.storage.remove('access_token');
let toast = this.toastController.create({
message: 'JWT removed',
duration: 3000
});
toast.then((toast) => toast.present());
}
}
Of course the last function is not needed for our auth system, it just shows: Once we clear the token from the storage we are still on the same page, but when we make the next request (here loading the special info again) we receive an error back from the server and because it means we are not authenticated, our service in the background will clear the token and throw us back to the login page!
To get the functions working simply add the needed buttons to your app/pages/inside/inside.page.html:
<ion-header>
<ion-toolbar color="primary">
<ion-title>Inside</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
Welcome to the members only area!
<p>{{ data }}</p>
<ion-button expand="full" (click)="loadSpecialInfo()">Load Special Info</ion-button>
<ion-button expand="full" (click)="logout()">Logout</ion-button>
<ion-button expand="full" (click)="clearToken()">Clear JWT</ion-button>
</ion-content>
That’s it for the Ionic JWT app and authentication system!
Conclusion
It doesn’t really matter in which language your server is written if the system uses JWT auth, you can very easily build an Ionic app that works with it without any problems.
Of course this tutorial only shows one way to achieve the desired results, there might be many more different solutions but the concept of the JWT auth will be pretty much the same across all of them.
If you enjoyed the series, leave a comment and don’t forget to share it on your preferred social media.
You can also find a video version of this article below.