How to logout user when token expires in react app

24,965

Solution 1

The issue you are facing is simple. Your AuthReducer takes in the initialState only once when its created. Now when you reload your app, everything is initialized again and the expiry is taken care of by your logic. However on Route change It doesn't re-evaluate your initialState.

However what you can do is while using setContext you can check for validation of expiry by decoding the token using jwtDecode and refresh the token if it expired and save in localStorage since this is executed on every request

const authLink = setContext(async () => {
  let token = localStorage.getItem('JWT_Token')
  const { exp } = jwtDecode(token)
  // Refresh the token a minute early to avoid latency issues
  const expirationTime = (exp * 1000) - 60000
  if (Date.now() >= expirationTime) {
    token = await refreshToken()
    // set LocalStorage here based on response;
  }
  return {
    // you can set your headers directly here based on the new token/old token
    headers: {
      ...
    }
  }
})

However since you wish to redirect to login page and not refresh token when the token expired you can make use of custom history object with Routes

src/history.js

import { createBrowserHistory } from 'history';
const history = createBrowserHistory()
export default history;

App.js

import history from '/path/to/history.js';
import { Router } from 'react-router-dom';

<AuthProvider>
  <Router history={history}>
    <div className="App wrapper">
      <Routes/>
    </div>
  </Router>
</AuthProvider>

and then in setContext you could do

import history from '/path/to/history';
const authLink = setContext(async () => {
  let token = localStorage.getItem('JWT_Token')
  const { exp } = jwtDecode(token)
  const expirationTime = (exp * 1000) - 60000
  if (Date.now() >= expirationTime) {
    localStorage.clear();
    history.push('/login');
  }
  return {
    // you can set your headers directly here based on the old token
    headers: {
      ...
    }
  }
})

Solution 2

For your your problem the solution might be like:

  • Remove the auth part from the context. (Bad practice)
  • Create a component with react-router subscribed to check the auth state of the user.
  • Render it in the main component.

authverify.component.js

import { withRouter } from "react-router-dom";

const AuthVerifyComponent = ({ history }) => {
  history.listen(() => {  // <--- Here you subscribe to the route change
    if (localStorage.getItem("JWT_Token")) {
      const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
      console.log(jwt_Token_decoded.exp * 1000);
      console.log(Date.now());
      if (jwt_Token_decoded.exp * 1000 < Date.now()) {
        localStorage.clear();
      } else {
        initialstate.user = jwt_Token_decoded;
      }
    }
  });
  return <div></div>;
};

export default withRouter(AuthVerifyComponent);

app.js

<AuthProvider>
  <Router>
    <div className="App wrapper">
      <Routes />
      <AuthVerifyComponent />
    </div>
  </Router>
</AuthProvider>;

Share:
24,965
manish thakur
Author by

manish thakur

Updated on August 25, 2021

Comments

  • manish thakur
    manish thakur over 2 years

    I am working on a app where I am using React as my front-end and React-apollo-graphql for my API calling.

    I am using react-hooks i.e in React 16.8 +.

    What I am doing

    I have crated a auth.js file where I am storing my values when user is loging in and also checking the token is it valid or not, (expiry I am checking), but that file is only loading my I am refreshing or reloading the page, That is not how it should work

    My auth.js file

    const initialstate = {
        user: null,
    };
    if (localStorage.getItem("JWT_Token")) {
        const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
        console.log(jwt_Token_decoded.exp * 1000);
        console.log(Date.now());
        if (jwt_Token_decoded.exp * 1000 < Date.now()) {
            localStorage.clear(); // this runs only when I refresh the page or reload on route change it dosent work
        } else {
            initialstate.user = jwt_Token_decoded;
        }
    }
    
    const AuthContext = createContext({
        user: null,
        login: (userData) => {},
        logout: () => {},
    });
    const AuthReducer = (state, action) => {
        switch (action.type) {
            case "LOGIN":
            return {
                ...state,
                user: action.payload,
            };
            case "LOGOUT":
            return {
                ...state,
                user: null,
            };
            default:
            return state;
        }
    };
        
    const AuthProvider = (props) => {
        const [state, dispatch] = useReducer(AuthReducer, initialstate);
        const login = (userData) => {
            localStorage.setItem("JWT_Token", userData.token);
            dispatch({
            type: "LOGIN",
            payload: userData,
            });
        };
        const logout = () => {
            localStorage.clear();
            dispatch({ action: "LOGOUT" });
        };
        
        return (
            <AuthContext.Provider
            value={{ user: state.user, login, logout }}
            {...props}
            />
        );
    };
        
    export { AuthContext, AuthProvider };
    

    As I have commented the line where I am checking the token expiry.

    My only issue is why it is working on page reload not on each route like we do in store file when we use Redux.

    My App.js

    <AuthProvider>
      <Router>
        <div className="App wrapper">
          <Routes/>
        </div>
      </Router>
    </AuthProvider>
    

    My index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import ApolloClient from 'apollo-boost'
    import { ApolloProvider } from '@apollo/react-hooks';
    import { InMemoryCache } from 'apollo-cache-inmemory';
        
    const client = new ApolloClient({
      uri: 'my url',
      cache: new InMemoryCache(),
    });
    ReactDOM.render(
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>,
      document.getElementById('root')
    );
    

    Important points

    As I am using react-apollo-graphql so do they provide ant Authentication flow ? like how redux does, we have to create a store file which will store our data

    I am using React 16.8 + so I am using react-hooks so here I am using use Reducer from that only.

    My only question is am I doing it right? I am open to other approaches.

    I have done authentication and authorization in Vue using Vuex there I use to create a store file which runs on ever route

    Same I have done with Redux, In my store file I use to store the states and all.

    Now if I am using react-hooks and react-apollo-graphql so no need to do this things with redux.

    I am using apollo-link-context for passing the header (Authorization) like below

    const authLink = setContext(() => {
      const token = localStorage.getItem('JWT_Token')
      return {
        headers:{
          Authorization: token ? `${token}` : ''
        }
      }
    });
    

    I think here I can check on each route or on each request if the token is valid or not ( check exp time) if it is invalid then I will logout and clear my local storage, Clearing the storage is not a big deal the main thing is how to redirect to login page.

  • manish thakur
    manish thakur almost 4 years
    I already have a separate route file where all routes are written, And will this run on each route check, I mean on each rout render ?
  • Yogesh Aggarwal
    Yogesh Aggarwal almost 4 years
    Yes, you can try that out. I have done that before but not tried now.
  • manish thakur
    manish thakur almost 4 years
    hey, basically you are saying to delete the auth.js file and put all my code inside the new component you have created AuthVerifyComponent ?
  • Yogesh Aggarwal
    Yogesh Aggarwal almost 4 years
    Nope, just move the verification part to the AuthVerifyComponent so that you can use the withRouter hook & rest will remain the same.
  • manish thakur
    manish thakur almost 4 years
    So there it says initial state is not defined, i have to pass the initial state also, How ?
  • manish thakur
    manish thakur almost 4 years
    hey your answer is fine I tried this and it is working fine but my one issue is it only runs when route changes, what if I am in a page and there I have a add or delete button with respective functionality, so suppose I have 2 minutes of exp time to my token and from last 2 minutes I am there now I click on add but my token is expired so it will be deleted when i ll change the route but here I am not changing the route I am in same route doing other stuff so it will cause issue.
  • Yogesh Aggarwal
    Yogesh Aggarwal almost 4 years
    Just use settimeout to expire the token. When the route changes, the component will re-render & the timer get reset (I guess)
  • manish thakur
    manish thakur almost 4 years
    no no that is not a cool thing to do, there is something called as http link in react-apollo graphql that will do i think
  • Yogesh Aggarwal
    Yogesh Aggarwal almost 4 years
    So, are you now sorted?
  • manish thakur
    manish thakur almost 4 years
    Actually in your case there is issue in initialstate as it throws error as initial state is not defined
  • Yogesh Aggarwal
    Yogesh Aggarwal almost 4 years
    Okay so what are you trying to say? 😄
  • manish thakur
    manish thakur almost 4 years
    I didn't get this refreshToken() I have not created this function, And I have tried this approach like when token is expored by checking with jwtDecode I want to logout i.e redirect to login page and clear token, but here I don't know how to use redirect or route.
  • Shubham Khatri
    Shubham Khatri almost 4 years
    Ok, updating my answer for this
  • manish thakur
    manish thakur almost 4 years
    hey '/path/to/history' what path I should put here ?
  • Shubham Khatri
    Shubham Khatri almost 4 years
    relative path to history.js file
  • manish thakur
    manish thakur almost 4 years
    I have not created a history file, I am using react useHistory Hook for this.
  • Shubham Khatri
    Shubham Khatri almost 4 years
    Since you want to use history outside of the component hierarchy you need to create the customhistory as I showed in the above code. You can continue to use useHistory in your custom components. Also note how the Router component usage is changed
  • manish thakur
    manish thakur almost 4 years
    Hey to use history the custome one do i need to install something because it throws error as Unexpected use of 'history'
  • Shubham Khatri
    Shubham Khatri almost 4 years
    yes, please install the history module yarn add history
  • manish thakur
    manish thakur almost 4 years
    Hey please check the chat I have one issue, with this approach
  • manish thakur
    manish thakur over 3 years
    Hey I am stuck on one problem from very long time could you please help me out with some approach stackoverflow.com/questions/63814645/…
  • Alvaro Carvalho
    Alvaro Carvalho over 2 years
    This might help someone in the future: useHistory can be imported from the react-router-dom, no need to add 'history'
  • j0k
    j0k about 2 years
    I did it a similar way to this but used useEffect inside AuthProvider, with pathName (from useLocation) as the condition to useEffect, rather than pathName === your_path... (using react router v5)
  • ZenG
    ZenG about 2 years
    how do we know whether the token is expired or not? via webhook push or pulling data from backend or simply create a setTimeout method to expire the token in the frontend.