React – How to Logout when Token is expired (JWT)

In previous post, we’ve used JWT for token based authentication (register, login, logout) in that, if token is expired, user cannot access restricted resource and he need to click on Logout button to refresh the UI and login again. This tutorial continues to show you how to force logout user when the Token is expired.

Related Posts:
In-depth Introduction to JWT-JSON Web Token
React JWT Authentication (without Redux) example
React + Redux: JWT Authentication example

Using React Hooks:
React – How to Logout when Token is expired (JWT)


How to check JWT Token expiry in React

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

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

I will show you the implementations of both ways.
– For 1, we check the token expiration every time the Route changes and call App component logout method.
– 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 need to read one of following tutorials first:
React JWT Authentication (without Redux) example
React + Redux: JWT Authentication example

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

Logout user when token is expired and Route changes

We need to do 2 steps:
– Create a component with react-router subscribed to check JWT Token expiry.
– Render it in the App component.

In src folder, create common/auth-verify.js file with following code:

import React, { Component } from "react";
import { withRouter } from "react-router-dom";

const parseJwt = (token) => {
  try {
    return JSON.parse(atob(token.split('.')[1]));
  } catch (e) {
    return null;
  }
};

class AuthVerify extends Component {
  constructor(props) {
    super(props);

    props.history.listen(() => {
      const user = JSON.parse(localStorage.getItem("user"));

      if (user) {
        const decodedJwt = parseJwt(user.accessToken);

        if (decodedJwt.exp * 1000 < Date.now()) {
          props.logOut();
        }
      }
    });
  }

  render() {
    return <div></div>;
  }
}

export default withRouter(AuthVerify);

Because we use BrowserRouter, we import withRouter and wrap the component with a HoC. Now props can access the history object’s properties and functions. Then we pass a callback to props.history.listen() for listening every Route changes.

To call a parent App component logOut() method from AuthVerify component, we need to pass the logOut() method as a prop:

<AuthVerify logOut={this.logOut}/>

Let’s put the AuthVerify component into App component like this.

import React, { Component } from "react";
import { Switch, Route, Link } from "react-router-dom";
...

import AuthVerify from "./common/auth-verify";

class App extends Component {
  constructor(props) {
    super(props);
    this.logOut = this.logOut.bind(this);

    this.state = {
      showModeratorBoard: false,
      showAdminBoard: false,
      currentUser: undefined,
    };
  }

  ...

  logOut() {
    AuthService.logout();
    this.setState({
      showModeratorBoard: false,
      showAdminBoard: false,
      currentUser: undefined,
    });
  }

  render() {
    ...

    return (
      <div>
        <nav className="navbar navbar-expand navbar-dark bg-dark">
          ...
        </nav>

        <div className="container">
          <Switch>
            <Route exact path={["/", "/home"]} component={Home} />
            <Route exact path="/login" component={Login} />
            <Route exact path="/register" component={Register} />
            <Route exact path="/profile" component={Profile} />
            <Route path="/user" component={BoardUser} />
            <Route path="/mod" component={BoardModerator} />
            <Route path="/admin" component={BoardAdmin} />
          </Switch>
        </div>

        <AuthVerify logOut={this.logOut}/>
      </div>
    );
  }
}

export default App;

Logout user if token is expired from response status

First we need to set up a global event-driven system, or a PubSub system, which allows us to listen and dispatch events from independent components.

An Event Bus implements the PubSub pattern, events will be fired from other components so that they don’t have direct dependencies between each other.

We’re gonna create Event Bus with three methods: on, dispatch, and remove.

common/EventBus.js

const eventBus = {
  on(event, callback) {
    document.addEventListener(event, (e) => callback(e.detail));
  },
  dispatch(event, data) {
    document.dispatchEvent(new CustomEvent(event, { detail: data }));
  },
  remove(event, callback) {
    document.removeEventListener(event, callback);
  },
};

export default eventBus;

on() method attachs an EventListener to the document object. The callback will be called when the event gets fired.
dispatch() method fires an event using the CustomEvent API.
remove() method removes the attached event from the document object.

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

src/App.js

import React, { Component } from "react";
import { Switch, Route, Link } from "react-router-dom";
...

import EventBus from "./common/EventBus";

class App extends Component {
  constructor(props) {
    super(props);
    this.logOut = this.logOut.bind(this);

    this.state = {
      showModeratorBoard: false,
      showAdminBoard: false,
      currentUser: undefined,
    };
  }

  componentDidMount() {
    const user = AuthService.getCurrentUser();

    if (user) {
      this.setState({
        currentUser: user,
        showModeratorBoard: user.roles.includes("ROLE_MODERATOR"),
        showAdminBoard: user.roles.includes("ROLE_ADMIN"),
      });
    }
    
    EventBus.on("logout", () => {
      this.logOut();
    });
  }

  componentWillUnmount() {
    EventBus.remove("logout");
  }

  logOut() {
    AuthService.logout();
    this.setState({
      showModeratorBoard: false,
      showAdminBoard: false,
      currentUser: undefined,
    });
  }

  render() {
    ...

    return (
      <div>
        <nav className="navbar navbar-expand navbar-dark bg-dark">
          ...
        </nav>

        <div className="container">
          <Switch>
            <Route exact path={["/", "/home"]} component={Home} />
            <Route exact path="/login" component={Login} />
            <Route exact path="/register" component={Register} />
            <Route exact path="/profile" component={Profile} />
            <Route path="/user" component={BoardUser} />
            <Route path="/mod" component={BoardModerator} />
            <Route path="/admin" component={BoardAdmin} />
          </Switch>
        </div>
      </div>
    );
  }
}

export default App;

Finally we only need to dispatch "logout" event in the components when getting Unauthorized response status.

components/board-user.component.js

...
import EventBus from "../common/EventBus";

export default class BoardUser extends Component {
  constructor(props) { ... }

  componentDidMount() {
    UserService.getUserBoard().then(
      response => { ... },
      error => {
        this.setState({
          content:
            (error.response &&
              error.response.data &&
              error.response.data.message) ||
            error.message ||
            error.toString()
        });

        if (error.response && error.response.status === 401) {
          EventBus.dispatch("logout");
        }
      }
    );
  }

  render() { ... }
}

Conclusion

Today we’ve known two ways to check check jwt token expiry in React and logout user when the Token is expired.

For the code base, you need to read one of following tutorials first:
React JWT Authentication (without Redux) example
React + Redux: JWT Authentication example

Using React Hooks instead:
React – How to Logout when Token is expired (JWT)

You can continue to build fullstack Authentication and Authorization system with:
React + Spring Boot: JWT Authentication example
React + Node.js Express: JWT Authentication example

Further Reading

Fullstack CRUD example:
React + Spring Boot + MySQL
React + Spring Boot + PostgreSQL
React + Spring Boot + MongoDB
React + Node.js Express + MySQL
React + Node.js Express + PostgreSQL
React + Node.js Express + MongoDB
React + Django

Source Code

You can find the complete source code on Github:
React (without Redux).
React + Redux.

Leave a Reply

Your email address will not be published. Required fields are marked *