Angular - Logout when Token is expired

Tien Nguyen - Sep 18 '22 - - Dev Community

In this Angular 14 tutorial, I will show you how to logout when JWT Token is expired. You also know two approaches to checking if JWT token is expired or not in Angular.

This tutorial is from BezKoder:
https://www.bezkoder.com/logout-when-token-expired-angular-14/

Check if JWT token is expired or not in Angular

There are two ways to check if Token is expired or not.

  • 1. Proactive strategy: get expiry time in JWT and compare with current time
  • 2. Reactive strategy: read response status from the server

I will show you the implementations of both approaches.
– For 1, we check the token expiration and call logout method/dispatch logout event.
– For 2, we dispatch logout event to App component when response status tells us the token is expired.

We're gonna use the code base for next steps. So you may need to read following tutorial first:
Angular 14 JWT Authentication & Authorization example

The Github source code is at the end of the tutorial.

Check the token expiration in Angular

With this approach, we get expiry time from JWT token (stored in Browser Local Storage or Session Storage) and compare with the current time.

private isTokenExpired(token: string) {
  const expiry = (JSON.parse(atob(token.split('.')[1]))).exp;
  return expiry * 1000 > Date.now();
}

ngOnInit() {
  if (this.isTokenExpired(token)) {
    // call logout method/dispatch logout event
  } else {
    // token is valid: send requests...
  }
}
Enter fullscreen mode Exit fullscreen mode

For more details about structure of a JWT, kindly visit:
In-depth Introduction to JWT-JSON Web Token

How about the Token is stored in HttpOnly Cookie that can't be accessed by JavaScript?
On client side, we don't check the JWT as cookie on server side, so we will use Interceptor to catch the HTTP request/response from token expiring, then send token refresh call and replay the HTTP request.

Token Refresh endpoint is implemented on the server side and it is a little bit complicated. For instructions:

In this post, we just logout when Token is expired.
For Refresh Token, please visit:
Angular Refresh Token with JWT example

Logout when Token is expired in Angular

Typically you don't check token validity on the client side (Angular) but catch the 401 response in the Interceptor. We will handle JWT token expiration using an HTTP_INTERCEPTOR provider. With the Interceptor, we can add the Bearer token to HTTP requests or handle errors.

We will dispatch logout event to App component when response status tells us the access token is expired.

First 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

_shared/event.class.ts

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

  constructor(name: string, value: any) {
    this.name = name;
    this.value = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {
  // ...

  eventBusSub?: Subscription;

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

  ngOnInit(): void {
    // ...

    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);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

import { StorageService } from '../_services/storage.service';
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 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()) {
        this.eventBusService.emit(new EventData('logout', null));
      }
    }

    return next.handle(request);
  }
}

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

In the code above, we:

  • intercept requests or responses before they are handled by intercept() method.
  • handle 401 error status on interceptor response (except response of /signin request)
  • emit "logout" event if user is logged in.

Logout when JWT Token is expired without Interceptor

This is another way to logout the user when Token is expired. This approach is not recommended because we have to catch 401 error status in every Http Request that accesses protected resources.

import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';
import { StorageService } from '../_services/storage.service';
import { EventBusService } from '../_shared/event-bus.service';
import { EventData } from '../_shared/event.class';

@Component({
  selector: 'app-board-user',
  templateUrl: './board-user.component.html',
  styleUrls: ['./board-user.component.css']
})
export class BoardUserComponent implements OnInit {
  content?: string;

  constructor(
    private userService: UserService,
    private storageService: StorageService,
    private eventBusService: EventBusService
  ) {}

  ngOnInit(): void {
    this.userService.getUserBoard().subscribe({
      next: data => {
        this.content = data;
      },
      error: err => {
        // ...

        if (err.status === 401 && this.storageService.isLoggedIn()) {
          this.eventBusService.emit(new EventData('logout', null));
        }
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Proactive token strategy and the Reactive token strategy have their own pros and cons.

401 will always be handled correctly in Reactive strategy, but before knowing the token is expired or not, it requires response from HTTP Request. If we provide a 10+ minute (or more) expiration period, the API calls aren't likely to really impose a burden on user.

Proactive strategy reduces the HTTP Request, because the token could expire before the request, but it adds overhead to each request.

Some people could validate the token before sending some HTTP requests, but also include 401 replay logic.

For Refresh Token:
Angular Refresh Token with JWT example

Source Code

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

Further Reading

Fullstack:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .