This past week, I was tackling a problem where I need to authenticate a React client with a GraphQL server using Amazon Cognito and had to handle auto refreshing the expired tokens.
Problem
What can we do when an API token has expired?
Just show it as an error to the user, which might be a bad user experience (maybe ask them to re-log in)
Check whether our token is valid before every request, which might not be a scalable option if we have many requests.
”Assume” that the token is valid, and if it turns out otherwise, we will handle refreshing the token and retrying the request.
In this article, I will go with the third option.
TL; DR
This what my final Apollo Link stack looks like
Solution
Amazon Cognito
Cognito is AWS’s user authentication solution, similar to other authentication provider such as Auth0, Okta, or Firebase.
When a user is logged in, it will return 3 JWT tokens: Access, Identity and Refresh tokens. Identity token is used to get user information such as username and email. Access token is used to get access to resources. Both tokens have short expiry, which is set to 1 hour by default. The other token, the refresh token, is different. It has a longer expiry date, and is used to “refresh” access and identity token. Learn more about JWT tokens here.
First things first, we need to set up an Apollo Client. Here we opt to use the individual packages provided by Apollo instead of apollo-boost package because we need flexibility to configure our Apollo Client later on.
Before we go further, I want to explain the concepts of link in Apollo. While Apollo have built in defaults in its request, it also gives us the ability to modify how our request should look like by exposing its network layer through a family of apollo-link-* packages. We have seen one in our code above, which uses the HttpLink provided by apollo-link-http package.
We will now focus more on building the Apollo Link stack. As discussed before, each link worked as a middleware and is able to modify the request or handle errors. Links can also be chained together. Apollo provides the ApolloLink.from([ Link1, Link2, ... , LinkN ]) constructor to chain multiple links.
We currently have one link, which is the HttpLink. This will always be in the link stack since it is the terminating link (the last link) that will fetch GraphQL results from a GraphQL endpoint over http connection.
Authentication
Most API endpoints are secured with authentication, so we need to somehow pass in a token or an API key to the request headers. We can do this by adding the setContext function from apollo-link-context package. The getAccessToken is a asynchronous function that return a promise to return the token.
Error handling
Okay, so our request now have the Authorization header. How do we handle Unauthenticated errors? We can use the onError function provided by the apollo-link-error package. The onError link will only run if there is an error.
So our request will look like this :
onSuccess:
onError:
Modify your code to include this part of the code. It creates a new link that will log whenever we get a 401 error, which indicates that our token has expired. A thing to note is the order in which the links are placed.
Handling retries
Currently, we only logged the error whenever we get an unauthenticated error. Now, we want to handle the refresh token and retry the request. As shown in the docs, we can retry a request using forward(operation). So we can try something like
Unfortunately,onError function provided to handle error does not support asynchronous request and only returns an Observable or a void. So we created a helper function to wrap the promise inside an Observable and changed the code to use our helper function. A thing to note here is that we need to wrap the refreshSession(callback) inside a promise which will only resolve after the callback has been executed. I need to do this because the API for AWS Amplify (which I am using as a client-side library to access AWS Cognito), is returning a function with a callback, and not a function that returns a promise. If your refreshSession returns a promise, then you should not need to wrap it again.
Note: AWS Cognito stores the token inside localStorage, so you can actually hack the problem by accessing the token(s) directly and synchronously without the hassle of wrapping promises into observables. However, I believe that it is more maintainable to access the token via the API provided.