In Par-1 we had implemented a basic angular authentication using the HTTP only cookie. Now we are going to enhance some features like 'Authentication Guard', 'HTTP Interceptor To Refresh The JWT Token Inside The HTTP Only Cookie', 'User Logout'.
shared/auth/auth-guard.ts:
src/shared/auth/auth.interceptor.ts:
Angular Route Guard For Authentication:
Problem 1:- After successful authentication, we reload our angular application, or the user closes the browser and then opens again, we can see our user information will be lost.
Problem 2:- Currently with our sample application, we can access any page means, if the user is not logged in also can access the 'dashboard' page, similarly if the user logged in can also access the login form page.
Solution:- Implementing Angular Route Guard for authentication.
In 'AuthService' let's add logic to load the authenticated user information either from the 'AuthService.userProfile' variable or from the browser local storage. Because if the application is reloaded then the info in the 'AuthService.userProfile' will be lost, so in that case, we get authenticated user information from the browser local storage.
src/shared/auth/auth.service.ts:
loadUserFromLocalStorage(): UserModel { if (this.userProfile.value.id == 0) { let fromLocalStorage = localStorage.getItem('user-profile'); if (fromLocalStorage) { let userInfo = JSON.parse(fromLocalStorage); this.userProfile.next(userInfo); } } return this.userProfile.value; }Let's create an angular route guard for authentication.
ng g class shared/auth/auth-guard --skip-tests
shared/auth/auth-guard.ts:
import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree, } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> { let userInfo = this.authService.loadUserFromLocalStorage(); if (route.data['userType'] === 'guest') { return true; } else if (route.data['userType'] === 'loged-in') { if (userInfo.id > 0) { return true; } this.router.navigate(['/']); return false; } else if (route.data['userType'] === 'non-loged-in') { if (userInfo.id === 0) { return true; } this.router.navigate(['/']); return false; } this.router.navigate(['/']); return false; } }
- (Line: 13) To create a route guard we need to implement 'CanActivate' from the '@angular/router'.
- (Line: 15-41) The 'canActivate' method gets executed for the routes that got registered with our angular guard in the 'AppRoutingModule'. So if this method returns 'true' then the application loads component respective to the route, if it returns 'false' then the angular application won't allow us to load the component.
- (Line: 23) Fetching the authenticated user information.
- (Line: 24-26) The 'route.data['userType']' is a custom property that we will configure in the routes in the 'ApprouteModule', so using this value we can define who can access routes. Here I had implemented value like 'guest' means all can consume the route.
- (Line: 26-31) The 'logged-in' value means only authentication users can access the route.
- (Line: 32-38) The 'non-loge-in' value means only users, not logged in can access the route.
src/app.module.ts:
import { AuthGuard } from './shared/auth/auth-guard'; // code hidden for display purpose const routes: Routes = [ { path: '', component: HomeComponent, canActivate: [AuthGuard], data: { userType: 'guest', }, }, { path: 'login', component: LoginComponent, canActivate: [AuthGuard], data: { userType: 'non-loged-in', }, }, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard], data: { userType: 'loged-in', }, }, ];Now register 'AuthGuard' into the providers of 'AppModule'.
src/app.module.ts:
import { AuthGuard } from './shared/auth/auth-guard'; // code hidden for display purpose @NgModule({ providers: [AuthGuard], })Note:- Now run the application check above mentioned 2 problems and we can observe both of them get resolved.
NestJS Refresh-Token API:
Our NestJS HTTP-Only cookie contains both 'Access Token' and 'Refresh Token'. So if the 'Access Token' expires we have to use 'RefreshToken'. So let's use the 'Refresh-Token' endpoint to regenerate the Cookie with new 'AccessToken' and 'Refresh Token'.
http://localhost:3000/refresh-token
HttpInterceptor To Invoke RefreshToken Endpoint On AccessToken Expires:
Let's implement the logic for Refresh Token API in 'AuthService'.
src/shared/auth/auth.service.ts:
refreshCookie() { return this.http.get('http://localhost:3000/refresh-token', { withCredentials: true, }); }Let's create our HttpInterceptor to invoke refresh token endpoint on access token expiration.
ng g class shared/auth/auth.interceptor --skip-tests
src/shared/auth/auth.interceptor.ts:
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { catchError, Observable, switchMap, throwError } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private route: Router) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { if (req.url.indexOf('/refresh-token') > -1) { return next.handle(req); } return next.handle(req).pipe( catchError((error) => { if (error.status == 401) { return this.handle401Error(req, next, error); } else { return throwError(() => error); } }) ); } private handle401Error( req: HttpRequest<any>, next: HttpHandler, originalError: any ) { return this.authService.refreshCookie().pipe( switchMap(() => { return next.handle(req); }), catchError((error) => { localStorage.removeItem('user-profile'); this.route.navigate(['/']); return throwError(() => originalError); }) ); } }
- (Line: 13) To create an interceptor we have to implement the 'HttpInterceptor' that loads from the '@angular/common/http'.
- (Line: 16-32) The 'interceptor()' method gets invoked for every HTTP API request from our angular application.
- (Line: 20-22) For the refresh token endpoint, we no need to check the user is authenticated or not.
- (Line: 24-30) The 'catchError' method gets executed for API error response.
- (Line: 25-27) Checking for 401 status code which means unauthorized exceptions.
- (Line: 34-50) The 'handle401Error()' method handles the unauthorized exception by invoking the refresh token endpoint, once the new cookie is created, again API's failed will be reinvoked.
src/app.module.ts:
import {HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './shared/auth/auth.interceptor'; // code hidden for display purpose @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true, }, ] })
NestJS Logout API:
Logout API clears the authentication cookie.
http://localhost:3000/logout
Logout In Angular Application:
Let's first add the 'Logout' API call logic in the 'AuthService'.
src/shared/auth/auth.service.ts:
logout() { return this.http.get('http://localhost:3000/logout', { withCredentials: true }); }In 'AppComponent' let invoke the 'Logout' API.
src/app.component.ts:
logOut() { this.auth.logout().subscribe({ next: () => { this.auth.userProfile.next({ email: '', firstName: '', id: 0, lastName: '', phone: '', }); localStorage.removeItem('user-profile'); this.router.navigate(['/']); }, }); }
- On successful logout API, we have to clear authenticated user information from our angular application like 'auth.userProfile' variable and browser local storage.
src/app.component.html:
<li class="nav-item text-dark" (click)="logOut()"> <button class="btn btn-primary" type="button">SignOut</button> </li>
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on JWT Authentication using the HTTP-Only Cookie in Angular application. using I love to have your feedback, suggestions, and better techniques in the comment section below.
Comments
Post a Comment