How to refresh JWT token using Apollo and GraphQL
Solution 1
The example given in the the Apollo Error Link documentation is a good starting point but assumes that the getNewToken()
operation is synchronous.
In your case, you have to hit your GraphQL endpoint to retrieve a new access token. This is an asynchronous operation and you have to use the fromPromise
utility function from the apollo-link package to transform your Promise to an Observable.
import React from "react";
import { AppRegistry } from 'react-native';
import { onError } from "apollo-link-error";
import { fromPromise, ApolloLink } from "apollo-link";
import { ApolloClient } from "apollo-client";
let apolloClient;
const getNewToken = () => {
return apolloClient.query({ query: GET_TOKEN_QUERY }).then((response) => {
// extract your accessToken from your response data and return it
const { accessToken } = response.data;
return accessToken;
});
};
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
case "UNAUTHENTICATED":
return fromPromise(
getNewToken().catch((error) => {
// Handle token refresh errors e.g clear stored tokens, redirect to login
return;
})
)
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
const oldHeaders = operation.getContext().headers;
// modify the operation context with a new token
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${accessToken}`,
},
});
// retry the request, returning the new observable
return forward(operation);
});
}
}
}
}
);
apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, httpLink]),
});
const App = () => (
<ApolloProvider client={apolloClient}>
<MyRootComponent />
</ApolloProvider>
);
AppRegistry.registerComponent('MyApplication', () => App);
You can stop at the above implementation which worked correctly until two or more requests failed concurrently. So, to handle concurrent requests failure on token expiration, have a look at this post.
Solution 2
Update - Jan 2022 you can see basic React JWT Authentication Setup from: https://github.com/bilguun-zorigt/React-GraphQL-JWT-Authentication-Example
I've also added the safety points to consider when setting up authentication on both the frontend and backend on the Readme section of the repository. (XSS attack, csrf attack etc...)
Original answer - Dec 2021
My solution:
- Works with concurrent requests (by using single promise for all requests)
- Doesn't wait for error to happen
- Used second client for refresh mutation
import { setContext } from '@apollo/client/link/context';
async function getRefreshedAccessTokenPromise() {
try {
const { data } = await apolloClientAuth.mutate({ mutation: REFRESH })
// maybe dispatch result to redux or something
return data.refreshToken.token
} catch (error) {
// logout, show alert or something
return error
}
}
let pendingAccessTokenPromise = null
export function getAccessTokenPromise() {
const authTokenState = reduxStoreMain.getState().authToken
const currentNumericDate = Math.round(Date.now() / 1000)
if (authTokenState && authTokenState.token && authTokenState.payload &&
currentNumericDate + 1 * 60 <= authTokenState.payload.exp) {
//if (currentNumericDate + 3 * 60 >= authTokenState.payload.exp) getRefreshedAccessTokenPromise()
return new Promise(resolve => resolve(authTokenState.token))
}
if (!pendingAccessTokenPromise) pendingAccessTokenPromise = getRefreshedAccessTokenPromise().finally(() => pendingAccessTokenPromise = null)
return pendingAccessTokenPromise
}
export const linkTokenHeader = setContext(async (_, { headers }) => {
const accessToken = await getAccessTokenPromise()
return {
headers: {
...headers,
Authorization: accessToken ? `JWT ${accessToken}` : '',
}
}
})
export const apolloClientMain = new ApolloClient({
link: ApolloLink.from([
linkError,
linkTokenHeader,
linkMain
]),
cache: inMemoryCache
});
Solution 3
If you are using JWT, you should be able to detect when your JWT token is about to expire or if it is already expired.
Therefore, you do not need to make a request that will always fail with 401 unauthorized.
You can simplify the implementation this way:
const REFRESH_TOKEN_LEGROOM = 5 * 60
export function getTokenState(token?: string | null) {
if (!token) {
return { valid: false, needRefresh: true }
}
const decoded = decode(token)
if (!decoded) {
return { valid: false, needRefresh: true }
} else if (decoded.exp && (timestamp() + REFRESH_TOKEN_LEGROOM) > decoded.exp) {
return { valid: true, needRefresh: true }
} else {
return { valid: true, needRefresh: false }
}
}
export let apolloClient : ApolloClient<NormalizedCacheObject>
const refreshAuthToken = async () => {
return apolloClient.mutate({
mutation: gql```
query refreshAuthToken {
refreshAuthToken {
value
}```,
}).then((res) => {
const newAccessToken = res.data?.refreshAuthToken?.value
localStorage.setString('accessToken', newAccessToken);
return newAccessToken
})
}
const apolloHttpLink = createHttpLink({
uri: Config.graphqlUrl
})
const apolloAuthLink = setContext(async (request, { headers }) => {
// set token as refreshToken for refreshing token request
if (request.operationName === 'refreshAuthToken') {
let refreshToken = localStorage.getString("refreshToken")
if (refreshToken) {
return {
headers: {
...headers,
authorization: `Bearer ${refreshToken}`,
}
}
} else {
return { headers }
}
}
let token = localStorage.getString("accessToken")
const tokenState = getTokenState(token)
if (token && tokenState.needRefresh) {
const refreshPromise = refreshAuthToken()
if (tokenState.valid === false) {
token = await refreshPromise
}
}
if (token) {
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
}
}
} else {
return { headers }
}
})
apolloClient = new ApolloClient({
link: apolloAuthLink.concat(apolloHttpLink),
cache: new InMemoryCache()
})
The advantage of this implementation:
- If the access token is about to expire (REFRESH_TOKEN_LEGROOM), it will request a refresh token without stopping the current query. Which should be invisible to your user
- If the access token is already expired, it will refresh the token and wait for the response to update it. Much faster than waiting for the error back
The disadvantage:
- If you make many requests at once, it may request several times a refresh. You can easily protect against it by waiting a global promise for example. But you will have to implement a proper race condition check if you want to guaranty only one refresh.
Solution 4
after checking this topic and some others very good on internet, my code worked with the following solution
ApolloClient,
NormalizedCacheObject,
gql,
createHttpLink,
InMemoryCache,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import jwt_decode, { JwtPayload } from 'jwt-decode';
import {
getStorageData,
setStorageData,
STORAGE_CONTANTS,
} from '../utils/local';
export function isRefreshNeeded(token?: string | null) {
if (!token) {
return { valid: false, needRefresh: true };
}
const decoded = jwt_decode<JwtPayload>(token);
if (!decoded) {
return { valid: false, needRefresh: true };
}
if (decoded.exp && Date.now() >= decoded.exp * 1000) {
return { valid: false, needRefresh: true };
}
return { valid: true, needRefresh: false };
}
export let client: ApolloClient<NormalizedCacheObject>;
const refreshAuthToken = async () => {
const refreshToken = getStorageData(STORAGE_CONTANTS.REFRESHTOKEN);
const newToken = await client
.mutate({
mutation: gql`
mutation RefreshToken($refreshAccessTokenRefreshToken: String!) {
refreshAccessToken(refreshToken: $refreshAccessTokenRefreshToken) {
accessToken
status
}
}
`,
variables: { refreshAccessTokenRefreshToken: refreshToken },
})
.then(res => {
const newAccessToken = res.data?.refreshAccessToken?.accessToken;
setStorageData(STORAGE_CONTANTS.AUTHTOKEN, newAccessToken, true);
return newAccessToken;
});
return newToken;
};
const apolloHttpLink = createHttpLink({
uri: process.env.REACT_APP_API_URL,
});
const apolloAuthLink = setContext(async (request, { headers }) => {
if (request.operationName !== 'RefreshToken') {
let token = getStorageData(STORAGE_CONTANTS.AUTHTOKEN);
const shouldRefresh = isRefreshNeeded(token);
if (token && shouldRefresh.needRefresh) {
const refreshPromise = await refreshAuthToken();
if (shouldRefresh.valid === false) {
token = await refreshPromise;
}
}
if (token) {
return {
headers: {
...headers,
authorization: `${token}`,
},
};
}
return { headers };
}
return { headers };
});
client = new ApolloClient({
link: apolloAuthLink.concat(apolloHttpLink),
cache: new InMemoryCache(),
});
user3043462
Updated on July 25, 2022Comments
-
user3043462 almost 2 years
So we're creating a React-Native app using Apollo and GraphQL. I'm using JWT based authentication(when user logs in both an activeToken and refreshToken is created), and want to implement a flow where the token gets refreshed automatically when the server notices it's been expired.
The Apollo Docs for Apollo-Link-Error provides a good starting point to catch the error from the ApolloClient:
onError(({ graphQLErrors, networkError, operation, forward }) => { if (graphQLErrors) { for (let err of graphQLErrors) { switch (err.extensions.code) { case 'UNAUTHENTICATED': // error code is set to UNAUTHENTICATED // when AuthenticationError thrown in resolver // modify the operation context with a new token const oldHeaders = operation.getContext().headers; operation.setContext({ headers: { ...oldHeaders, authorization: getNewToken(), }, }); // retry the request, returning the new observable return forward(operation); } } } })
However, I am really struggling to figure out how to implement getNewToken(). My GraphQL endpoint has the resolver to create new tokens, but I can't call it from Apollo-Link-Error right?
So how do you refresh the token if the Token is created in the GraphQL endpoint that your Apollo Client will connect to?
-
Herku about 4 yearsThe onError link runs after a request. I don't think you can simply forward to try again. Ideally, you can determine if your current token is still valid in the frontend e.g. by looking at the
exp
claim in a JWT. Then you could use this excellent link: github.com/newsiberian/apollo-link-token-refresh -
Herku about 4 yearsYou can call your GraphQL enpoint using
window.fetch
. This is a bit more work but should be no problem for a single query. SimplyPOST
to the endpoint with a JSON object containingquery
and optionallyvariables
andoperation
.
-
-
Hansel over 3 yearsHow would you then update the cookie with the new token?
-
Léon Logli over 3 years@MustKillBill This workflow is for header-based authentification where the jwt can be accessed, set or stored by the client. In cookie-based authentication, the client cannot access cookies with JavaScript as they are usually marked HTTPOnly. So it's up to the server to send cookies by using the Set-Cookie HTTP header which instructs the web browser to store the cookie and send it back in future requests to the server.
-
Marc Simon almost 3 yearsThis is in react native btw, but the logic is the same for web
-
Stephen over 2 yearsWould it be possible for you to share the full code you are using for the apollo interface? Ideally with one example, such as login? I cant put my finger exactly how this would work with my existing code.
-
Stephen over 2 yearsThanks for sharing that great example. It looks like you are using django-graphql-jwt.domake.io/index.html which uses a single token for the auth, whereas I am using django-graphql-auth.readthedocs.io which uses a separate refresh token. I added in a refresh token to your code and am trying to get it working. Wish me luck :)
-
Stathis Ntonas over 2 yearsshouldn't this:
currentNumericDate + 1 * 60
be(currentNumericDate + 1) * 60
? -
BIlguun Zorigt over 2 years@Stathis Ntonas 1 * 60 is just adding 1 minute, meaning if token is not going to expire in 1 minute, refreshing is not needed.
-
Stathis Ntonas over 2 years@earthguestg then in this case
currentNumericDate + 60
is enough, no need for1 * 60
-
Martin F. over 2 yearsVery elegant solution.
-
Rahul Purohit about 2 years@earthguestg Great solution. How does it take care of concurrent requests? WIll using 1 promise have impact on the ux?
-
BIlguun Zorigt about 2 years@Rahul Purohit Apollo itself handles the concurrent requests very well, so it's just using that. The 1 promise is only holding those concurrent requests till new token is resolved. So there's no impact on the ux that I'm aware of.