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:
Handle JWT Token expiration in React with Hooks
Contents
- How to check JWT Token expiry in React
- Logout user when token is expired and Route changes
- react-router-dom v5
- react-router-dom v6
- Logout user if token is expired from response status
- react-router-dom v5
- react-router-dom v6
- Conclusion
- Further Reading
- Source Code
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 Typescript 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.
react-router-dom v5
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 AuthService from "./services/auth.service";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;
react-router-dom v6
From the react-router-dom v6, the support for history
has been deprecated. So we need a wrapper (HOC) that can use new useful hooks.
In src folder, create common/with-router.js file with following code:
import { useLocation, useNavigate, useParams } from "react-router-dom";export const withRouter = (Component) => { function ComponentWithRouterProp(props) { let location = useLocation(); let navigate = useNavigate(); let params = useParams(); return <Component {...props} router={{ location, navigate, params }} />; } return ComponentWithRouterProp;};
Next, we create AuthVerify
Component that uses location
for listening the route change:
common/auth-verify.js
import React, { useEffect } from "react";import { withRouter } from "./with-router";const parseJwt = (token) => { try { return JSON.parse(atob(token.split('.')[1])); } catch (e) { return null; }};const AuthVerify = (props) => { let location = props.router.location; useEffect(() => { const user = JSON.parse(localStorage.getItem("user")); if (user) { const decodedJwt = parseJwt(user.accessToken); if (decodedJwt.exp * 1000 < Date.now()) { props.logOut(); } } }, [location]); return <div></div>;};export default withRouter(AuthVerify);
Finally, we need to pass the logOut()
method as a prop in App
component:<AuthVerify logOut={this.logOut}/>
src/App.js
import React, { Component } from "react";import { Routes, Route, Link } from "react-router-dom";...import AuthService from "./services/auth.service";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 mt-3"> <Routes> <Route path="/" element={<Home />} /> <Route path="/home" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/profile" element={<Profile />} /> <Route path="/user" element={<BoardUser />} /> <Route path="/mod" element={<BoardModerator />} /> <Route path="/admin" element={<BoardAdmin />} /> </Routes> </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 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() { ... }}
Finally we only need to import EventBus
in App component and listen to "logout"
event.
react-router-dom v5
src/App.js
import React, { Component } from "react";import { Switch, Route, Link } from "react-router-dom";...import AuthService from "./services/auth.service";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() { ... 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;
react-router-dom v6
src/App.js
import React, { Component } from "react";import { Routes, Route, Link } from "react-router-dom";...import AuthService from "./services/auth.service";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() { ... 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 mt-3"> <Routes> <Route path="/" element={<Home />} /> <Route path="/home" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/profile" element={<Profile />} /> <Route path="/user" element={<BoardUser />} /> <Route path="/mod" element={<BoardModerator />} /> <Route path="/admin" element={<BoardAdmin />} /> </Routes> </div> </div> ); }}export default App;
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 Typescript JWT Authentication (without Redux) example
– React + Redux: JWT Authentication example
Using React Hooks instead:
Handle JWT Token expiration in React with Hooks
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
- React Router Guide
- React Components
- In-depth Introduction to JWT-JSON Web Token
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
Simplify import statement with:
Absolute Import in React
Source Code
You can find the complete source code on Github:
– React (without Redux).
– React Typescript (without Redux).
– React + Redux.