In Part-1 we have implemented steps for jwt authentication in angular application. This is a continuation of Part-1, our main goals here to use the access token as a key for authorization header to access secured endpoints and refresh token to re-issue the access token on the expiration.
Angular Interceptors are helpful to manipulate the request before going to the server. Using the angular interceptors we will add an authorization header to our request. Using interceptors will affect all API requests of our application. To skip the effects of interceptor for any particular request we need to manually handle it inside of the interceptors.
NestJS(Nodejs) 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 angular application.
http://localhost:3000/todos
Authorization Header To Access Secured Endpoint:
Now let's try to consume the secured 'Todos' endpoint by adding the access token to the request header.
Let's create a 'Todos' service.
src/app/services/todos.service.ts:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable() export class TodosService{ constructor(private http:HttpClient){} getTodos(){ return this.http.get("http://localhost:3000/todos"); } }
- Created a 'Todos' endpoint.
src/app/dashboard/dashboard.module.ts:
import { NgModule } from '@angular/core'; import { DashboardRoutingModule } from './dashboard-routing.module'; import { DashboardComponent } from './dashboard.component'; import { TodosService } from '../services/todos.service'; import { CommonModule } from '@angular/common'; @NgModule({ imports:[ DashboardRoutingModule, CommonModule ], declarations:[ DashboardComponent ], providers:[ TodosService ] }) export class DashboardModule {}
- Registered 'TodosService' into DashboardModule.
src/app/dashboard/dashboard.component.ts:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../services/auth.service'; import { TodosService } from '../services/todos.service'; @Component({ templateUrl:'dashboard.component.html' }) export class DashboardComponent implements OnInit{ userName:string = ''; todos:any; constructor(private authService:AuthService, private todosService:TodosService){} ngOnInit(): void { this.authService.userInfo.subscribe(value => { if(value){ this.userName = value.username; } }) this.loadTodos(); } loadTodos(){ this.todosService.getTodos() .subscribe( (value) => { this.todos = value; }, (error) => { console.log('failted to load todos') } ) } }
- (Line: 10) The 'todos' component variable to store the data from the API.
- (Line: 19) Invoking to 'loadTodos()' method from 'ngOnInit' life cycle method so that 'Todos' data gets loaded on 'dashboard' page laods.
src/app/dashboard/dashboard.component.html:
<h3>Welcome {{userName}} !</h3> <div *ngIf="todos"> <h5>My Todos</h5> <ul > <li *ngFor="let todo of todos">{{todo}}</li> </ul> </div>Let's try to consume the secured Todos API without any authorization header for the request.
Angular Interceptors are helpful to manipulate the request before going to the server. Using the angular interceptors we will add an authorization header to our request. Using interceptors will affect all API requests of our application. To skip the effects of interceptor for any particular request we need to manually handle it inside of the interceptors.
src/app/interceptors/auth.token.interceptors.ts:
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; @Injectable() export class AuthTokenInterceptors implements HttpInterceptor{ intercept(req: HttpRequest<any>, next: HttpHandler) :Observable<HttpEvent<any>> { const access_token = localStorage.getItem("access_token"); const transformedReq = req.clone({ headers: req.headers.set('Authorization', `bearer ${access_token}`) }); return next.handle(transformedReq); } }
- (Line: 8) Fetching jwt access token from the localStorage.
- (Line: 9) Modifying the request by adding the authorization header.
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 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 Refresh Token:
At the time of successful login, API returns us both 'access_token', 'refresh_token' in the response. Now along with 'access_token', we also need to store 'refresh_token' in browser localStorage.
Now update the 'userLogin' method in AuthService to save 'refresh_token' and 'access_token' expiration DateTime.
src/app/service/auth.services.ts:
userLogin(login: any): Observable<boolean> { if (login && login.username && login.password) { return this.http.post("http://localhost:3000/auth/login",login).pipe( map((data: any) => { if (!data) { return false; } localStorage.setItem('access_token', data.access_token); localStorage.setItem('refresh_token', data.refresh_token); const decodedUser = this.jwtHelper.decodeToken(data.access_token); localStorage.setItem('expiration', decodedUser.exp); this.userInfo.next(decodedUser); return true; }) ); } return of(false); }
- (Line: 9) Stores the 'refresh_token' into the local storage.
- (Line: 11) Store jwt 'access_token' expiration value in local storage so that it can be used for deciding to call refresh token API.
NestJS API Refresh Token Endpoint URL:- http://localhost:3000/auth/refreshtoken Payload:{ "access_token":"", "refresh_token":"" }Now let's create a new method to call the 'Refresh Token' endpoint in AuthService.
src/app/services/auth.service.ts:
callRefershToken(payload){ return this.http.post("http://localhost:3000/auth/refreshtoken",payload); }Now let's update the AuthTokenInterceptor to call refresh token API whenever the jwt access token expires.
src/app/interceptors/auth.token.interceptor.ts:
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { AuthService } from '../services/auth.service'; import { switchMap, map, flatMap, take } from 'rxjs/operators'; import { JwtHelperService } from '@auth0/angular-jwt'; @Injectable() export class AuthTokenInterceptors implements HttpInterceptor { jwtHelper = new JwtHelperService(); constructor(private authService: AuthService) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { if (req.url.indexOf('/refreshtoken') > -1) { return next.handle(req); } const access_token = localStorage.getItem('access_token'); if (access_token) { const expiration = localStorage.getItem('expiration'); if (Date.now() < Number(expiration) * 1000) { const transformedReq = req.clone({ headers: req.headers.set('Authorization', `bearer ${access_token}`), }); return next.handle(transformedReq); } const payload = { access_token: access_token, refresh_token: localStorage.getItem('refresh_token'), }; return this.authService.callRefershToken(payload).pipe( switchMap((newTokens: any) => { localStorage.setItem('access_token', newTokens.access_token); localStorage.setItem('refresh_token', newTokens.refresh_token); const decodedUser = this.jwtHelper.decodeToken( newTokens.access_token ); localStorage.setItem('expiration', decodedUser.exp); this.authService.userInfo.next(decodedUser); const transformedReq = req.clone({ headers: req.headers.set( 'Authorization', `bearer ${newTokens.access_token}` ), }); return next.handle(transformedReq); }) ); } else { return next.handle(req); } } }
- (Line: 20-22) Here we by-passing interceptor for the 'Refresh Token' endpoint API call. If we don't add this logic to our interceptor we will face big trouble like infinite loops of observable and will throw an error about maximum stack exceeded for observables.
- (Line: 26-30) Checks for jwt access token expiration, if access token not expired we allow interceptor to add an authorization header to HTTP calls.
- (Line: 36) If the jwt access token expires, then calls the refresh token endpoint.
- Upon receiving a response from the refresh token endpoint, we will store the new access token and refresh token and expiration will save to browser local storage, then will allow interceptor to use our new access token value for authorization header.
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information on the refresh token implementation in angular application. I love to have your feedback, suggestions, and better techniques in the comment section below.
Hi there, can you share the code for this example.Thanks
ReplyDelete