In Part-1 we have implemented steps for jwt authentication in Ionic5 angular application. This is a continuous article of Part-1, our main goals here are to use access token in the authorization header and refresh token implementation for user authentication.
NestJS(Node.js) Todos API:
In Part-1 we discussed steps to set up the Nestjs API. In that API we have a secured endpoint called 'Todos'. In the next step, we are going to consume this 'Todo' API from our Ionic application.
http://localhost:3000/todos
Ionic Application Use Authorization Header:
Now let's try to consume the secured 'Todos' endpoint by adding the access token to the header.
Let's now create a TodoService as below.
src/app/services/todo.service.ts:
import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from 'rxjs'; @Injectable() export class TodoServie { constructor(private http:HttpClient){ } getTodos():Observable<any>{ return this.http.get("http://localhost:3000/todos"); } }Now import the 'TodoService' into the 'DashboardModule' providers arrays like below.
src/app/dashboard/dashboard.module.ts:
import { TodoServie } from '../services/todos.service'; // code hidden for display purpose @NgModule({ providers:[TodoServie] }) export class DashboardPageModule {}Update the dashboard page to display the 'Todos' items.
src/app/dashboard/dashboard.page.ts:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../services/auth.service"; import { TodoServie } from "../services/todos.service"; @Component({ selector: "app-dashboard", templateUrl: "./dashboard.page.html", styleUrls: ["./dashboard.page.scss"], }) export class DashboardPage implements OnInit { userName = ""; todos:any; constructor(private authService: AuthService, private todoService:TodoServie) {} ngOnInit() { this.authService.userInfo.subscribe((user) => { alert(user); if (user) { this.userName = user.username; } }); this.fetchTodos(); } fetchTodos(){ this.todoService.getTodos().subscribe( (data) => { this.todos = data; }, (error) => { alert('failed fetch todos') } ) } }
- The 'fetchTodos()' method invokes the 'todos' endpoint. Since we want to show all todos on navigating to the page so invoke the 'fetchTodos()' method in the 'ngOnInit' method.
<ion-header> <ion-toolbar> <ion-title>{{userName}} - Dashboard</ion-title> </ion-toolbar> </ion-header> <ion-content> <h1>Welcome!</h1> <div *ngIf="todos"> My Todos <ul> <li *ngFor="let todo of todos">{{todo}}</li> </ul> </div> </ion-content>Finally, we will create the angular interceptor to add the access token as the authorization header to the API request.
src/app/interceptors/auth.token.interceptor.ts:
import {Injectable} from '@angular/core'; import { HttpInterceptor,HttpRequest, HttpHandler,HttpEvent } from '@angular/common/http'; import { Observable, from } from 'rxjs'; import {AuthService} from '../services/auth.service'; import { map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthTokenInterceptor implements HttpInterceptor{ constructor(private authService:AuthService){} intercept(req:HttpRequest<any>, next :HttpHandler): Observable<HttpEvent<any>> { return from(this.authService.getAccessToken()) .pipe( map(value => { return value; }), switchMap(token => { const transformedReq = req.clone({ headers: req.headers.set('Authorization', `bearer ${token}`) }); return next.handle(transformedReq); }) ) } }
- (Line: 9) Injected 'AuthService' into our interceptor.
- The method 'authService.getAccessToken()' is a promise return type to make it observable we used rxjs 'from' operator. Using the 'pipe' operator reads the flow of observable. Using the 'map' operator reads the 'access token' value, then creating a new observable flow by using the 'switchMap' operator. Finally returns the Observable of 'HttpEvent' type for the interceptor by adding the authorization header.
src/app/app.module.ts:
import {HTTP_INTERCEPTORS} from '@angular/common/http'; import { AuthTokenInterceptor } from './interceptors/auth.token.interceptor'; // code hidden for display puprose @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthTokenInterceptor, multi: true, } ] }) export class AppModule {}
Refresh Token Flow:
- Refresh Token is a random string key that will be created along with the JWT access token and return to the valid client on successful logging in.
- Now for all subsequent requests will use the access token, but the access token is a short-lived token whereas the refresh token lives more time than the access token.
- On the expiration of the access token, the user instead of authenticating himself again passing his user name and password, the user can send the refresh token.
- The server on receiving a refresh token, first it validates against the storage(database, cache, etc).
- For a valid refresh token server will create a new access token and refresh token(like when authenticate using user name and password) return it to the user.
Integrate RefreshToken:
At the time of successful login, API returns us both 'access token' and 'refresh token' in the response. Now along with 'access token', 'refresh token' also need to be saved in the device storage.
Let's update the 'userlogin()' method in the AuthService file to save the refresh token.
src/app/services/auth.service.ts:
useLogin(login: any): Observable<boolean> { if(login && login.email && login.password){ var payload={ username:login.email, password:login.password }; return this.http.post("http://localhost:3000/auth/login",payload).pipe( map((response:any)=>{ console.log(response); this.storage.set('access_token',response.access_token); this.storage.set('refresh_token', response.refresh_token); var decodedUser = this.jwtHelper.decodeToken(response.access_token); this.userInfo.next(decodedUser); console.log(decodedUser); return true; }) ) } return of(false); }
- (Line: 11) Refresh token storing to device local storage.
src/app/service/auth.service.ts:
getRefreshToke(){ return this.storage.get("refresh_token"); }Now add method in AuthService to invoke the refresh token API.
src/app/services/auth.service.ts:
callRefreshToken(payload){ return this.http.post("http://localhost:3000/auth/refreshtoken", payload); }Update our 'AuthTokenInterceptor' to invoke the refresh token API whenever the server return 401(unauthorized) status.
src/app/interceptors/auth.token.interceptor.ts:
import { Injectable } from "@angular/core"; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, } from "@angular/common/http"; import { Observable, from, combineLatest } from "rxjs"; import { AuthService } from "../services/auth.service"; import { map, switchMap, catchError, flatMap } from "rxjs/operators"; import { JwtHelperService } from "@auth0/angular-jwt"; import { Storage } from "@ionic/storage"; @Injectable() export class AuthTokenInterceptor implements HttpInterceptor { jwtHelper = new JwtHelperService(); constructor( private authService: AuthService, private readonly storage: Storage ) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { return combineLatest( this.authService.getAccessToken(), this.authService.getRefreshToke(), (access_token, refresh_toke) => { return { access_token, refresh_toke, }; } ).pipe( switchMap((value) => { const transformedReq = req.clone({ headers: req.headers.set( "Authorization", `bearer ${value.access_token}` ), }); return next.handle(transformedReq).pipe( catchError((error) => { if (error.status === 401) { return this.authService.callRefreshToken(value).pipe( switchMap((newtoken: any) => { this.storage.set("access_token", newtoken.access_token); this.storage.set("refresh_token", newtoken.refresh_toke); var decodedUser = this.jwtHelper.decodeToken( newtoken.access_token ); this.authService.userInfo.next(decodedUser); const transformedReq = req.clone({ headers: req.headers.set( "Authorization", `bearer ${newtoken.access_token}` ), }); return next.handle(transformedReq); }) ); } }) ); }) ); } }
- (Line: 16) Initialized 'JwtHelperService' instance that loads from '@auth0/angular-jwt' library.
- (Line: 19) Injected Ionic storage.
- The 'combineLatest' is an rxjs observable which gets invoked when all its observables get loaded with the result.
- (Line: 26-27) Reading the access token and refresh token values from the ionic store observables.
- (Line: 43) The 'next' variable of type 'HttpHandler', registered with 'catchError' observable. In our case we need to catch error on status '401'(unauthorized).
- (Line: 44) Checking error status, because we want to invoke refresh token API only on access token expired.
- (Line: 45) Invoked refresh token API.
- (Line: 47-48) On successful completion refresh token API response data that contains a new access token and refresh token are updated to ionic storage.
- (Line: 53-58) The request has been modified to add an authorization header with a new access token value.
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information on the JWT authentication in Ionic5 angular application. I love to have your feedback, suggestions, and better techniques in the comment section below.
Comments
Post a Comment