Angular – Logout when Token is expired

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.
I will write Angular tutorial for Refresh Token soon.

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 three 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 
  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);
      
    );
  

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.error) 
          try 
            const res = JSON.parse(err.error);
            this.content = res.message;
           catch 
            this.content = `Error with status: $err.status - $err.statusText`;
          
         else 
          this.content = `Error with status: $err.status`;
        

        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.

Source Code

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

Further Reading

Fullstack: