Angular 16 Refresh Token with JWT & Interceptor example

With previous posts, we’ve known how to build JWT Authentication and Authorization in a Angular 16 Application. In this tutorial, I will continue to show you way to implement Angular 16 Refresh Token before Expiration with Http Interceptor and JWT.

Related Posts:
In-depth Introduction to JWT-JSON Web Token
Angular 16 JWT Authentication & Authorization example
Angular 16 Logout when Token is expired


Overview

The diagram shows flow of how we implement Angular 16 Refresh Token with JWT and Http Interceptor example.

angular-16-refresh-token-jwt-interceptor-flow

– A refresh Token will be provided in HttpOnly Cookie at the time user signs in.
– If Angular 16 Client accesses protected resources, a legal JWT must be stored in HttpOnly Cookie together with HTTP request.
– With the help of Http Interceptor, Angular App can check if the access Token (JWT) is expired (401), sends /refreshToken request to receive new access Token and use it for new resource request.

This is how Refresh Token works in our Angular example:
1- User sends request with legal JWT:

angular-16-jwt-refresh-token-request

2- JWT is expired, our Application automatically sends Token Refresh request, then uses new Access Token right after that.

angular-16-refresh-token-jwt-handle-error

angular-16-refresh-token-jwt-example

angular-16-refresh-token-jwt-new-access-token

The Back-end server for this Angular 16 Client can be found at:

We’re gonna implement Token Refresh feature basing on the code from previous post, so you need to read following tutorial first:
Angular 16 JWT Authentication & Authorization example

Add Refresh Token function in Angular Service

Firstly, we need to create refreshToken() function that uses HttpClient to send HTTP Request with refreshToken in the body.

_services/auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

const AUTH_API = 'http://localhost:8080/api/auth/';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(private http: HttpClient) {}

  // register, login, logout

  refreshToken() {
    return this.http.post(AUTH_API + 'refreshtoken', { }, httpOptions);
  }
}

Angular 16 Refresh Token with Interceptor

To implement silent refresh JWT token, we need to use an Http Interceptor to check 401 status in the response and call Token Refresh API with the Refresh Token stored in HttpOnly Cookie.

Let’s open _helpers/auth.interceptor.ts and write following code:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS, HttpErrorResponse } from '@angular/common/http';

import { StorageService } from '../_services/storage.service';
import { AuthService } from '../_services/auth.service';

import { Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';

import { EventBusService } from '../_shared/event-bus.service';
import { EventData } from '../_shared/event.class';

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
  private isRefreshing = false;

  constructor(
    private storageService: StorageService,
    private authService: AuthService,
    private eventBusService: EventBusService
  ) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    req = req.clone({
      withCredentials: true,
    });

    return next.handle(req).pipe(
      catchError((error) => {
        if (
          error instanceof HttpErrorResponse &&
          !req.url.includes('auth/signin') &&
          error.status === 401
        ) {
          return this.handle401Error(req, next);
        }

        return throwError(() => error);
      })
    );
  }

  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;

      if (this.storageService.isLoggedIn()) {
        return this.authService.refreshToken().pipe(
          switchMap(() => {
            this.isRefreshing = false;

            return next.handle(request);
          }),
          catchError((error) => {
            this.isRefreshing = false;

            if (error.status == '403') {
              this.eventBusService.emit(new EventData('logout', null));
            }

            return throwError(() => error);
          })
        );
      }
    }

    return next.handle(request);
  }
}

export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
];

In the code above, we:
– intercept requests or responses before they are handled by intercept() method.
– handle 401 status on interceptor response (except response of /signin request).
– if the user is logged in, call AuthService.refreshToken() method.
– if the API returns response with 403 error (the refresh token is expired), emit 'logout' event.

Implement EventBus Service

The logout event will be dispatched to App component when response status tells us the access token is expired.

We need to set up a global event-driven system, or a PubSub system, which allows us to listen and dispatch (emit) events from independent components so that they don’t have direct dependencies between each other.

We’re gonna create EventBusService with two methods: on and emit.

_shared/event-bus.service.ts

import { Injectable } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { EventData } from './event.class';

@Injectable({
  providedIn: 'root'
})
export class EventBusService {
  private subject$ = new Subject<EventData>();

  constructor() { }

  emit(event: EventData) {
    this.subject$.next(event);
  }

  on(eventName: string, action: any): Subscription {
    return this.subject$.pipe(
      filter((e: EventData) => e.name === eventName),
      map((e: EventData) => e["value"])).subscribe(action);
  }
}

_shared/event.class.ts

export class EventData {
  name: string;
  value: any;

  constructor(name: string, value: any) {
    this.name = name;
    this.value = value;
  }
}

Now you can emit event to the bus and if any listener was registered with the eventName, it will execute the callback function action.

Next we import EventBusService in App component and listen to "logout" event.

src/app.component.ts

import { Component } from '@angular/core';
import { Subscription } from 'rxjs';
import { StorageService } from './_services/storage.service';
import { AuthService } from './_services/auth.service';
import { EventBusService } from './_shared/event-bus.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  private roles: string[] = [];
  isLoggedIn = false;
  showAdminBoard = false;
  showModeratorBoard = false;
  username?: string;

  eventBusSub?: Subscription;

  constructor(
    private storageService: StorageService,
    private authService: AuthService,
    private eventBusService: EventBusService
  ) {}

  ngOnInit(): void {
    this.isLoggedIn = this.storageService.isLoggedIn();

    if (this.isLoggedIn) {
      const user = this.storageService.getUser();
      this.roles = user.roles;

      this.showAdminBoard = this.roles.includes('ROLE_ADMIN');
      this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR');

      this.username = user.username;
    }

    this.eventBusSub = this.eventBusService.on('logout', () => {
      this.logout();
    });
  }

  logout(): void {
    this.authService.logout().subscribe({
      next: res => {
        console.log(res);
        this.storageService.clean();

        window.location.reload();
      },
      error: err => {
        console.log(err);
      }
    });
  }
}

Finally we only need to emit "logout" event in the Angular Http Interceptor like previous section.

Conclusion

Today we know how to implement Angular 16 JWT Refresh Token before expiration using Http Interceptor with 401 status code. For your understanding the logic flow, you should read one of following tutorial first:
Angular 16 JWT Authentication & Authorization example

The Back-end server for this Angular 16 Client can be found at:

Before running the backend server, you need to add minor configuration:
– Spring Boot:

/* In AuthController.java */
// @CrossOrigin(origins = "*", maxAge = 3600)
@CrossOrigin(origins = "http://localhost:8081", maxAge = 3600, allowCredentials="true")

/* In TestController.java */
// @CrossOrigin(origins = "*", maxAge = 3600)
@CrossOrigin(origins = "http://localhost:8081", maxAge = 3600, allowCredentials="true")

– Node.js Express:

/* In server.js */
// app.use(cors());
app.use(
  cors({
    credentials: true,
    origin: ["http://localhost:8081"],
  })
);

They configure CORS for port 8081, so you have to run Angular Client command instead:
ng serve --port 8081

Source Code

You can find the complete source code for this tutorial on Github.

Further Reading

Fullstack:
Angular 16 + Spring Boot: JWT Authentication & Authorization example
Angular 16 + Node.js Express: JWT Authentication & Authorization example