Handling JWT authentication errors with React Apollo
August 11, 2018
Apollo’s GraphQL platform is so fun to use, making your application’s data layer fresh, easy to use, and massively flexible. I’ve written a previous post series on Apollo client and server which covered some basic setup and a authToken middleware implementation, but this post will go a little deeper in to global error handling with authentication.
One reason for this short post is that I think the documentation for anything other than simple “recipes”, including global auth error handling, is lacking, especially for the Higher Order Component (HOC) implementation of React Apollo which has since been dropped from the website docs in favour of using render props. This has meant a little trial and error has been involved in me getting to a auth error handling solution I’m happy with in a recent side project. I’m a big fan of the HOC way to use React Apollo, so I’m sticking with it!
The aim
As mentioned, I wanted to implement global error handling for an authentication section of a web app I’m developing. Specifically, I wanted the app to use JWT’s (JSON Web Tokens) for authentication, have the server check the JWT on each request sent from the web app, and when the user’s token expires, the app should automatically log the user out and throw an error message.
JWT authentication with Apollo
First I’ll explain a bit about the authentication setup. The app relies on JWT for authentication which is a common way to verify a user without using server side sessions. The JWT’s are created on the server and sent to the client when logging in, and are sent on each subsequent request.
Here’s a quick snapshot of my main app file:
// App.js
...
const httpLink = new HttpLink({
uri: 'http://localhost:1337/graphql'
});
// the auth token is sent to the server on each request due to this middleware
const authMiddleware = new ApolloLink((operation, forward) => {
const token = localStorage.getItem('authToken');
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}));
return forward(operation);
})
const appCache = new InMemoryCache()
const client = new ApolloClient({
link: from([
authMiddleware,
httpLink
]),
cache: appCache
});
class App extends React.Component {
public render() {
return (
<ApolloProvider client={client} >
<Header />
<Main />
</ApolloProvider>
);
}
}
export default App;
I’ve used middleware to get the users authToken from local storage and send to the server on each request. Then, on the server the token is checked. I’ve used the express-jwt
package on my Node/Express server to verify the token, and the server sends a 401 error back to the app when the token has expired.
Sending this 401 error code back is important as this is how I make the app react to an expired token. I’m not going in to detail about the server side implementation here.
Handling authentication errors
In order to keep the app more secure, it’s common to have a JWT expire after a certain period of time. Once this happens, you’d typically want to either refresh the token or log the user out. I’ve covered refreshing tokens in previous posts, but only recently implemented what I think is a great way to log users out when their token has expired.
With React, you’d typically handle errors on a per component basis with Apollo. This may look something like:
// ListItems.js
const addListItemWrapper = graphql(addListQuery, {
props: (mutate) => ({
addListItem: (todoDetails) => {
mutate({
variables: { ...todoDetails },
}).catch((error) => {
// do checks here for server response and log out user if JWT is invalid
})
},
}),
})
This approach will work, and you can easily use your chosen router (React Router for example) to log the user out from here, but I found this to be a repetitive and disjointed approach as this check/logout functionality would have to be added to each mutation which required the user to be logged in.
Instead, I was trying to find an approach which would leverage React’s built in component based architecture whilst being flexible enough to re-use and limit repetition. You guessed it - Higher Order Components.
The solution
In order to ensure errors are caught globally instead of per component query/mutation, you can use Apollo Link Error. This can be added alongside other links in the main app file, and allows us to catch all errors sent back from the server, including network errors and GraphQL specific errors.
Once we’ve established the user’s JWT has expired using Apollo Link Error, the rest of the app should be informed that the user needs to be logged out. In order to do this, Apollo has another handy link package called Apollo Link State. This link is great for local state management, similar to Redux, but whereas Redux uses actions and reducers, link state performs GraphQL style queries and mutations to local state.
Firstly, let’s set up the link state:
// App.js
... // imports
... // other links (HTTP Link and auth middleware)
const appCache = new InMemoryCache()
const stateLink = withClientState({
cache: appCache,
resolvers: {},
// set default state, else you'll get errors when trying to access undefined state
defaults: {
authStatus: {
__typename: 'authStatus',
status: 'loggedOut'
},
}
});
const client = new ApolloClient({
link: from([
authMiddleware,
stateLink, // include the new state link in the ApolloClient
httpLink
]),
cache: appCache
});
class App extends React.Component {
public render() {
return (
<ApolloProvider client={client} >
<Header />
<Main />
</ApolloProvider>
);
}
}
export default App;
The app begins with a default state set by Apollo Link State. The idea is that the local state will keep track of the user’s auth status, so the user’s auth needs to be set to loggedIn
when they log in to the app:
// Login.js
... // after the login mutation returns a success...
cache.writeData({
data: {
authStatus: {
__typename: 'authStatus',
status: 'loggedIn',
},
},
});
As the rest of the application has access to the local state also, the Apollo Link Error can be used to update the local state once the JWT has expired:
// App.js
... // other imports
... // other links (HTTP Link and auth middleware)
const appCache = new InMemoryCache()
const stateLink = withClientState({
cache: appCache,
resolvers: {},
defaults: {
authStatus: {
__typename: 'authStatus',
status: 'loggedOut'
},
}
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) => {
if (message === "Unauthorized") {
// every 401/unauthorized error will be caught here and update the global local state
localStorage.removeItem('authToken');
appCache.writeData({
data: {
authStatus: {
__typename: 'authStatus',
status: 'loggedOut',
},
},
});
}
});
}
});
const client = new ApolloClient({
link: from([
authMiddleware,
errorLink, // include the new error link in the ApolloClient
stateLink, // include the new state link in the ApolloClient
httpLink
]),
cache: appCache
});
class App extends React.Component {
public render() {
return (
<ApolloProvider client={client} >
<Header />
<Main />
</ApolloProvider>
);
}
}
export default App;
At this point the local state is updating on log in and token expiration, but the next step is to actually perform a router redirect back to the login page. This last section depends on how you handle your routing generally, but I’m using React Router in my current side project so I’ll show how I have integrated in to the popular React Router library.
As React Router only performs page changes inside a component in a declarative way, using HOC’s is a perfect way to subscribe any component to the authentication error handling and logging users out when their JWT expires:
// AuthRouteHoc.js
import gql from 'graphql-tag';
import * as React from 'react';
import { compose, graphql } from 'react-apollo';
import { Redirect } from 'react-router-dom';
const AuthRoute = () => (WrappedComponent) => {
return class extends React.Component {
constructor(props) {
super(props);
}
public render() {
const { data } = this.props
return data.authStatus.status === 'loggedOut' ? <Redirect to={`/`} /> : <WrappedComponent {...this.props} />;
}
};
}
const GET_AUTH = gql`
query authStatus {
authStatus @client {
status
}
}
`;
export default compose(graphql(GET_AUTH), AuthRoute());
The HOC is composed with React Apollo, allowing us to access the local state, specifically the auth status which we are updating when the server recognizes the JWT has expired. The @client
tag is added to the GraphQL query to instruct a query to local state rather than the server, and the response to this query is passed down as props to the render()
method of the AuthRoute HOC. This query, as with all other Apollo client queries, is bound in such a way that updates to the data are instantly updated elsewhere in the application. This means that as soon as the local state is updated by Apollo Link Error, the HOC wrapped around each of our chosen components will auto update and either render the component, or redirect the user to the login screen:
// AuthRouteHoc.js
import gql from 'graphql-tag';
import * as React from 'react';
import { compose, graphql } from 'react-apollo';
import AuthRoute from '../components/AuthRouteHoc';
class List extends React.Component {
public render() {
return (
<div>
// ... component meat and potatoes
</div>
);
}
}
// ... graphQL queries and React Apollo mutation/query code
export default compose(..., AuthRoute)(List);
The AuthRoute
component just needs to be imported and added to the composition of all components which you want to have this authentication system in place. This will usually be all private areas of your application - perhaps everywhere other than the login and register pages.
The elegance of this solution is that it isn’t repetitive, it’s concise, and it uses built in capabilities of both React and React Apollo. Also, it’s extremely flexible because any functionality or data massaging can be done in the HOC and will instantly apply to all subscribed components! One way to elaborate on this is showing error messages, which can be passed through to the login screen as props to show a specific message.
I hope someone found this helpful as there are a small number of posts I’ve found which are trying to achieve a similar outcome but go about it in very different ways. Let me know if you can think of ways to improve or I’ve missed something out. Thanks for reading!
Senior Engineer at Haven