Home > Mobile >  using JWTs saved in localstorage with react context
using JWTs saved in localstorage with react context

Time:12-27

Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:

const App = () => {
  const authCtx = useContext(AuthState);
  return(
    <AuthStateProvider>
      <BrowserRouter>
        <Switch>
          {!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
          {authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
          <Redirect from="/" to="/admin/myDashboard" />
        </Switch>
      </BrowserRouter>
    </AuthStateProvider>
)};

Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").

const AuthState = React.createContext({
  userName: "",
  isLoggedIn: false,
  authToken: null,
});

const AuthStateProvider = (props) => {
  let token = null;  
  if(localStorage.getItem("token")) return token = localStorage.getItem("token");

  const [ user, setUser ] = useState({
    userName: "Anonymous",
    isLoggedIn: false,
    authToken: token,
  });
  
  const autoLogin = useCallback( async () => {
    try {
      const response = await axios({
        method:'post',
        url: 'http://127.0.0.1:3001/authEn'
        headers: {
          "Content-Type": "application/json",
          "Authentication": user.authToken
        }
      });

      if(response.status === 200){
        //code to update context using setUser state handler
      } else {
        throw new Error("request failed");
      }
    } catch (e) {
      console.log(e.message);
    }
  });

  useEffect( async () => {
    await autoLogin();
  }, [autoLogin]);
  
  return (
    <AuthState.Provider
      value={{
        userName: user.userName,
        isLoggedIn: user.isLoggedIn,
        authToken: user.authToken
      }}    
    >
      {props.children}
    </AuthState.Provider>
  );
}

(I've excluded the code for my setUser handler to try and keep this short.) So the problem is that as of right now, I'm just trying to see that the application can

A: check for stored token on initial page load / reload

B: Navigate you to either logIn or Admin page accordingly.

The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)

CodePudding user response:

I think this is a bad practice to make conditions in the Switch. Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)

  1. First way with ProtectedRoute

Pass isProtected in props if your wrapped route requires authentification

// ProtectedRoute.js

import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';

const ProtectedRoute = ({
  isProtected,
  children,
}) => {
  const { isLoggedIn } = useContext(AuthState);

  if (isProtected && !isLoggedIn) {
    return <Redirect to='/login' />;
  }

  return children
};

export default ProtectedRoute;

Then in your switch:

<Switch>
{ /* Other routes */ }
  <ProtectedRoute isProtected>
    <Route path="/admin" component={Admin} />
  </ProtectedRoute>
</Switch>

  1. HOC

// withAuth

import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';

const withAuth = (WrappedComponent: any) =>
  function (props: any) {

    const { isLoggedIn ) = useContext(AuthState);

    if (!isLoggedIn) {
      <Redirect to='/login' />
    }

    return <WrappedComponent {...props} />;
  }

export default withAuth;

Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.

Example:

const NeedAuth = () => (
  <div>Hello I need auth</div>
);

export default withAuth(NeedAuth)

CodePudding user response:

I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!

  • Related