Search code examples
javascriptreactjsauthenticationjwtgoogle-oauth

What is the best practice for storing/using GoogleLogin JWT tokens in front-end react


I am building a react application and I am currently leveraging Google OAuth to allow users to sign into the application and storing the information using useState. The following component is used inside my Navbar component

import React, { useState} from 'react';
import { googleLogout } from '@react-oauth/google';
import { GoogleLogin } from '@react-oauth/google';

const Google = () => {
    const [profile, setProfile] = useState({});
    const [profileLoaded, setProfileLoaded] = useState(false);
    const [credentials, setCredentials] = useState({});

    // log out function to log the user out of google and set the profile array to null
    const logOut = () => {
        googleLogout();
        setProfileLoaded(false);
        setProfile(null);
    };

    function fail(error) {
        console.log(error);
        setProfileLoaded(false);
    }

    function getUser(googleResponse) {
        if (googleResponse?.credential) {
            setCredentials(googleResponse.credential);
            const token = googleResponse.credential;
            const decoded = jwt_decode(token);
            setProfile(decoded);
            setProfileLoaded(true);
        }

    }

    return (
        <div className='login'>
            <div className='google-button'>
                {
                    profileLoaded ?
                        (<img
                            className='profile-image'
                            src={profile.picture}
                            referrerPolicy="no-referrer"
                            alt="profile icon"/>) :
                        (<GoogleLogin
                            type='icon'
                            shape='pill'
                            onSuccess={(codeResponse) => getUser(codeResponse)}
                            onError={() => fail("Login Failed")}
                            />)
                }
            </div>
            {
                profileLoaded ?
                    (<button className='logout-button' onClick={logOut}>
                        Log out
                    </button>)
                    : (<></>)
            }
        </div>
    )
}

export default Google;

I want to be able to use this information from state in other components to enable/disable buttons if the user is not logged in. Additionally I need to pass the encoded JWT back to my API so that I can verify the token and validate the user and track usage statistics. But when I attempt to access these variables in my other components they are always undefined.

    const [profile, setProfile] = useState({});
    const [profileLoaded, setProfileLoaded] = useState(false);
    const [credentials, setCredentials] = useState({});

I've attempted using useState to share this information but it always comes up undefined. I also attempted using a useEffect block to update local variables when a user logs in but it also comes up undefined. I've thought about passing the data around as props but it seems messy and would require a significant restructure of the login mechanism/hierarchy, Which I'm fine with doing if that is the correct answer, but it doesn't seem like it would be.

I've spent a significant amount of time googling around about the issue but many of the responses are "don't authenticate in your ui" or "use local storage" I want to build this app to industry standards and handle authentication and user information securely. How should this information be handled in my front-end application so it can be accessed in other components securely? Do I need to add redux to my project for this? Thanks.


Solution

  • You need to use context if you want to be able to use the states across your components without passing props around.

    It's very simple to set up : Create a context.js file:

    import React, { createContext, useContext, useState } from "react";
    
    const Context = createContext();
    
    export const StateContext = ({ children }) => {
      const [profile, setProfile] = useState({});
     return (
        <Context.Provider
          value={{
            profile,
            setProfile
          }}
        >
          {children}
        </Context.Provider>
      );
    };
    export const useStateContext = () => useContext(Context);
    

    Now you wrap your application App.js with the context provider:

    import { StateContext } from "../util/context";
    
    return (
        <StateContext>
          <App/>
        </StateContext> 
    

    And that's it, now you can import profile and setProfile and use them globally wherever you want :

    import { useStateContext } from "../util/context";
    
    const { profile, setProfile} = useStateContext();
    

    Also this doesn't make your state persistent, it only makes it accessible globally by your components, to make it persistent you can use cookies to store the JWT and enable the security flags that you need.

    Security flags resource: https://www.invicti.com/learn/cookie-security-flags/