In my last post I set up my SPA with Laravel Sanctum & Apollo GraphQL client. The next step after that was to authenticate the user & keep that user logged in.

Local Storage vs Session Cookies

I am no expert in SPA or front-end but I do know that storing sensitive info in local storage is generally a bad idea. Yet, a lot of tutorials & articles I came across while researching for the "right way" to do this stored tokens & user authentication information in local storage. In my opinion, you should not store anything in local storage that you would not want publically available, such as passwords, JWTs (more on this later), sensitive info, API keys, etc. You might have a very secure app but one of the third-party libraries you have as a dependency might have a vulnerability at any point in the future that could allow attackers to read the local storage data & collect the information that should be private.

I know that most SPAs are stateless and rely on JWTs to handle authentications, but you need to be aware of the security issues that come with it. In most cases you will probably be fine just storing JWTs in local storage, if your app is vulnerable to XSS and attacker is able to run some Javascript code, JWTs would be least of your worries I think since they can do a lot more harm. Anyways, OWASP is a good place to check security related stuff.

In my case, this was easy to solve because my back-end (Laravel GraphQL API) is on the same domain as the SPA so using the session cookies was the right choice. I used Laravel Sanctum & it works really great. If I were to move my SPA somewhere else then I would either have a proxy API between the SPA & the main app API or expire JWT tokens faster & issue new ones to the user if the user is still active.

Let's See Some Code

I created a custom react hook that handles authentication. It simply sets the flag within the context so that the user can browse through the pages. If the user is not logged in & manually flips the flag through developer tools, not a big deal because when the request is made to the API, the API also checks if the user is authenticated. If the user is not authenticated it halts & signals react to redirect the user to the login page.

Here is the useAuth hook:

const useAuth = () => {  
    const {isAuthenticated, setAuthenticatedUser} = useContext(AppContext)  
    const [logIn, {loading: loggingIn}]           = useMutation(LogInMutation)  
    const [logOut, {loading: loggingOut}]         = useMutation(LogOutMutation)  
    const {parseResponse}                         = useGraphQLErrorHandler()  
    const {showError}                             = useAlertNotification()  
  
    const handleLogIn = (email, password) => {  
        if (! isAuthenticated) {  
            logIn({variables: {input: {email, password}}}).then(res => {  
                if (res.data.logIn) {  
                    setAuthenticatedUser(res.data.logIn)  
                }  
            }).catch(error => {  
                showError(parseResponse(error).firstError(), "bottom center")  
            })  
        }  
    }  
  
    const handleLogOut = () => {  
        if (isAuthenticated) {  
            logOut().then(res => {  
                if (res.data.logOut === "ok") {  
                    setAuthenticatedUser(null)  
                }  
            }).catch(error => {  
                showError(parseResponse(error).firstError())  
            })  
        } else {  
            window.location = '/login'  
        }  
    }  
  
    return {  
        handleLogIn,  
        loggingIn,  
        handleLogOut,  
        loggingOut  
    }  
}

Then I simply attach handleLogIn to the onClick of the Log In button. This is the GraphQL mutations for log in & log out:

import { gql } from "apollo-boost"  
  
export const LogInMutation = gql`  
    mutation($input: LoginInput!) { 
        logIn(input: $input) { 
            id 
            name 
            email 
        } 
    }
`  
  
export const LogOutMutation = gql`  
    mutation { 
        logOut 
    }
`

And this is my Router component:

const MainRouter = props => {
    const {isAuthenticated} = useContext(AppContext)  
  
    if (! isAuthenticated) {  
        return (  
            <Switch>  
                <Route path="/login" component={ Login } />  
                <Route><Redirect to="/login" /></Route>  
            </Switch>  
        )  
    }

    return (  
        <Switch>
            <PrivateRoute
              path="/dashboard"
              layout={ SidebarLayout }
              layoutProps={ {title: "Dashboard"} }
              component={ Dashboard }
              isAuthenticated={ isAuthenticated }
            />
            <Route path="/logout" component={ SignOut } />
        </Switch>
    )
}

Get notified as soon as new content is published