Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly redirect after catching authentication failure with Apollo and React

I'm writing a react app that uses apollo-client and I'm using apollo-link-error to catch authentication errors globally. I'm using createBrowserHistory for browser history manipulation and redux to manage my app state.

On authentication error I want to redirect the user to the /login page. However, doing so with history.push('/login') and forceRefresh:false changes the URL but doesn't actually navigate inside my app.

If I use forceRefresh:true it works, but the app is completely restarted, which I'd like to avoid.

const errorLink = onError(({ graphQLErrors, networkError }) => {
    if(graphQLErrors[0].extensions.code == "UNAUTHENTICATED") {
        // with forceRefresh:true this works, but causes a nasty 
        // reload, without the app doesn't navigate, only the url changes 
        history.push('/login')
    }
});

`

let links = [errorLink, authLink, httpLink];    
const link = ApolloLink.from(links);

    const client = new ApolloClient({
        link: link,
        cache: new InMemoryCache(),
        connectToDevTools: true,
    });

I think the problem is that I'm not using redux-router methods to navigate (so the app stays the same even though the url changes)

Q: how do I get a redux history object similar to using withRouter() when I'm not inside a component? What is the proper way to handle this situation?

like image 467
bpylearner Avatar asked Jan 23 '19 14:01

bpylearner


People also ask

Is react Apollo deprecated?

The react-apollo package has been deprecated, and the functionality offered by each of the above packages can now be accessed from @apollo/client directly: @apollo/react-hooks -> now available directly from @apollo/client.

What is Graphqlerror?

graphQLErrors and the response data is set to undefined even if the server returns data in its response. This means network errors and GraphQL errors result in a similar response shape. This is the default error policy. ignore. graphQLErrors are ignored ( error.

How do you connect Apollo client to react?

To connect Apollo Client to React, you will need to use the ApolloProvider component exported from @apollo/react-hooks . The ApolloProvider is similar to React's Context. Provider . It wraps your React app and places the client on the context, which allows you to access it from anywhere in your component tree.


1 Answers

Short summary of one possible solution:

  1. Wrap all routes that require authentication inside <ProtectedRoute> component that redirects unauthenticated users to login page. ProtectedRoute-component just check if user has valid token, if not redirects user.
  2. Inside error link first remove token, or somehow unvalidate it, then call location.reload()

Detailed implementation below.

I was not able to find any straightforward solution. In normal cases, to redirect user I use react-router navigate() hook. Inside the error link I found no way to use react-hooks.

However, I managed to solve the actual problem. I implemented ProtectedRoute component which wraps all the parts of the application which requires authentication:

type ProtectedRouteProps = {
    path: string;
    toRedirect: string;
};

export const ProtectedRoute: FunctionComponent<ProtectedRouteProps> = ({
    path,
    toRedirect,
    children,
}) => {
    return isAuthenticated() ? (
        <Route path={path}>
            {children}
        </Route>
    ) : (
        <Navigate to={{ pathname: toRedirect }} />
    );
};

type ValidToken = string;
type ExpiredToken = 'Expired token'
type NullToken = '' | null

export type JwtTokenType = (ValidToken | ExpiredToken | NullToken )
export const isNullToken = (token: JwtTokenType) : boolean => {
    return (token === '' || token === null)
}

export const isExpiredToken = (token: JwtTokenType) : boolean => {
    return token === "Expired token"
}

export const isAuthenticated = () : boolean => {
    let token = getTokenFromCookies();
    return !(isExpiredToken(token) || isNullToken(token));
}

I use it as follows:

<Routes>
   <Route path="login" element={<LoginPage />} />
   <ProtectedRoute path="/*" toRedirect="login">
      // protected routes here
   </ProtectedRoute>
</Routes>

To handle logouts and redirects for unauthenticated user I implemented two functions:

// Use this in normal cases
export function useHandleLogout(): () => void {
    const navigate = useNavigate();
    // maybe call other hooks
    });
    function handleLogout() {
        navigate("/login");
        removeToken();
        // do other stuff you want
    }
    return handleLogout;
}

// Use this inside error-link
export const handleLogoutWithoutHook = () => {
    // Logout without hook
    removeToken();
    // do other stuff required when logout
    // eslint-disable-next-line no-restricted-globals
    location.reload();
    // location.reload() after token removed affects user redirect
    // when component is wrapped inside <ProtectedRoute> component

};

export const removeToken = () => {
    Cookies.remove("jwt-token")
}

And finally inside the error link:

export const errorLink =  onError(
    ({ graphQLErrors, networkError, operation, forward }) => {
        if (graphQLErrors) {
            for (let err of graphQLErrors) {
                if (err.message.includes('AnonymousUser')) {
                    handleLogoutWithoutHook()
                    return
                }
                if (err.message.includes('Signature has expired')) {
                    handleLogoutWithoutHook()
                }
                console.log(err.message)
            }
        }
        return forward(operation)
    }
);
like image 101
Viljami Avatar answered Oct 09 '22 11:10

Viljami