Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to logout user when token expires in react app

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.

like image 308
manish thakur Avatar asked May 25 '20 07:05

manish thakur


People also ask

How do you logout if token is expired react?

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.

How do you check if JWT Token is expired in react?

There are two ways to check if Token is expired or not. 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.

What happens when a JWT Token expires?

The JWT access token is only valid for a finite period of time. Using an expired JWT will cause operations to fail. As you saw above, we are told how long a token is valid through expires_in . This value is normally 1200 seconds or 20 minutes.


2 Answers

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: {
      ...
    }
  }
})
like image 197
Shubham Khatri Avatar answered Sep 22 '22 10:09

Shubham Khatri


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

like image 36
Yogesh Aggarwal Avatar answered Sep 20 '22 10:09

Yogesh Aggarwal