Apollo 19: Authentication Patterns
easy⏱ 5 mincourseapollo
Auth link chain
The setContext link intercepts every GraphQL operation and adds headers before the request is sent. For authentication, you read the JWT token from storage and attach it as an Authorization: Bearer <token> header. The link chain typically looks like:
authLink → errorLink (catch 401) → retryLink → httpLink
The error link watches for authentication errors (401 or specific GraphQL error codes) and can trigger a token refresh flow before retrying the failed operation.
import { setContext } from '@apollo/client/link/context';
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const client = new ApolloClient({
link: from([authLink, errorLink, httpLink]),
cache: new InMemoryCache(),
});
Add JWT auth header
Create an authLink using setContext that reads a JWT token from localStorage and adds it as a Bearer token in the Authorization header. Create an errorLink using onError that checks for a 401 status or UNAUTHENTICATED GraphQL error code. When detected, attempt to refresh the token using a REFRESH_TOKEN mutation, update localStorage, and retry the original operation using forward(operation). Build a login form that stores the token on success.
Refresh token flow
When a JWT expires, the server returns a 401 or an UNAUTHENTICATED error. Your error link should:
- Queue all pending operations.
- Call a
refreshToken endpoint with the refresh token.
- Update localStorage with the new access token.
- Retry all queued operations with the new token.
- If refresh fails, redirect to login.
Use a promise queue to prevent multiple simultaneous refresh requests when several queries fail at once.
let isRefreshing = false;
let pendingRequests = [];
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors?.some(e => e.extensions?.code === 'UNAUTHENTICATED')) {
if (!isRefreshing) {
isRefreshing = true;
refreshToken().then(newToken => {
localStorage.setItem('authToken', newToken);
pendingRequests.forEach(cb => cb());
pendingRequests = [];
isRefreshing = false;
});
}
return new Observable(observer => {
pendingRequests.push(() => {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${localStorage.getItem('authToken')}`,
},
});
forward(operation).subscribe(observer);
});
});
}
});
resetStore on logout
When a user logs out, you must clear the Apollo cache to prevent the next user from seeing stale data. client.resetStore() clears the cache AND refetches all active queries — but since the user is logged out, those refetches will fail. Use client.clearStore() instead, which clears without refetching. Also clear localStorage tokens and redirect to the login page. This three-step logout (clearToken → clearStore → redirect) is the standard pattern.