Auth0 and Apollo GraphQL handling token expiry

Seems like there is an abundance of articles out there to wire up Auth0 SPA library to a react with Apollo GraphQL application but none of them seem to explain how to handle the token expiry scenario (not easily anyway). Majority of the articles I found firstly didn’t involve Auth0 and secondly they are based on 401 responses from a server in the GraphQL ErrorLink middleware followed by a complex fromPromise calls to obtain a new token and then retry the original calls.

After a few attempts at this pattern, I had no luck. So I decided to change the strategy from handling 401 server responses, to instead check the expiry date on then token, so if expired call getTokenSilently to get a new token. Simple.

Basic setup

This youtube video is a really good example to follow along to setup the basic auth0 react Provider. It leaves you with only having to figure out which configuration values to apply in your application.

Backend

The tech stack used here as an example uses a Koa server with a route /api/graphql that is requires an auth token. The middleware auth can be applied now by using the koa-jwt package along with jwks-rsa. The youtube video above provides a walkthrough on setting up authorization on graphQL operations. This does definitely make it more flexible.

Looking at the middleware code:

import { Context, Next } from 'koa';
import jwt from 'koa-jwt';
import jwtrsa from 'jwks-rsa';


export default function ({ auth: { domain, audience } }) {
  return jwt({
    secret: jwtrsa.koaJwtSecret({
      jwksUri: `https://${domain}/.well-known/jwks.json`,
      cache: true,
      cacheMaxEntries: 5,
    }),
    audience: audience,
    issuer: `https://${domain}/`,
    algorithms: ['RS256'],
  }).unless({ path: [/^\/api\/(playground)/] });
}

You may notice that the Playground path is excluded. Now, in the koa app setup we just add the required middleware:

import Koa from 'koa';
import config from './config';
import authMiddleware from './authMiddleware';
import { graphQLServer, graphQLPlayground } from './graphql';

const app = new Koa();
app.use(mount('/api', authMiddleware(config)));

// setup 
graphQLServer.applyMiddleware({ app, path: '/api/graphql' });
graphQLPlayground.applyMiddleware({ app, path: '/api/playground' });

// start it up
app.listen(4000);

Frontend

The grunt of the work to handle this situation happens in the Apollo graphQL client middleware. But I’ll start providing the code examples from the GraphQL provider:

import createClient from './createClient';

const GraphQLProvider = ({ children }: Props) => {
  const auth = useAuth()!;

  const { getTokenSilently } = auth;

  const client = createClient({ getTokenSilently});

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

export default GraphQLProvider;

The createClient method returns a new GraphQL client. The key part of this snippet is the order in which the links are created. The auth0Link comes first and is responsible to always ensure that there is a valid token. The authLink is only responsible for attaching the token in to the http headers.

export default function createClient({ getTokenSilently }) {
  const auth0Link = createAuth0Link({ getTokenSilently });
  const errorLink = ...;
  const webSocketLink = ...;

  const splitLink = split(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
    },
    webSocketLink,
    httpLink,
  );

  const authLink = setContext((_, { headers, auth0Token }) => ({
    headers: {
      ...headers,
      ...(auth0Token ? { Authorization: `Bearer ${auth0Token}` } : {}),
    },
  }));

  const link = from([auth0Link, errorLink, authLink, splitLink]);
  const cache = createInMemoryCache();
  const apolloClient = new ApolloClient<NormalizedCacheObject>({
    link,
    cache,
  });

  return apolloClient;
}

So let’s have a look at the auth0Link:

import jwtDecode, { JwtPayload } from 'jwt-decode';

let cachedToken: string;
let tokenExpiry: Date;

export const getAuthToken = async ({ getTokenSilently }) => {
  if (cachedToken && tokenExpiry > new Date()) {
    return cachedToken;
  }

  console.log('Requesting new token. Old one expired');
  const newToken = await getTokenSilently();
  cachedToken = newToken;
  const { exp } = jwtDecode<JwtPayload>(newToken);
  tokenExpiry = new Date(exp * 1000);
  return cachedToken;
};

As mentioned at the start of this post, the getTokenSilently is the method we need to invoke from Auth0 to get a new jwt token. But we only want to get a new one if the cached one has expired. And this is quite simple by using the jwt-decode library and storing the token expiry date when we receive a new one.

Happy coding…