Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to refresh JWT token using Apollo and GraphQL

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?

like image 575
user3043462 Avatar asked Apr 20 '20 16:04

user3043462


People also ask

How do I refresh JWT tokens?

For the refresh token, we will simply generate a UID and store it in an object in memory along with the associated user username. It would be normal to save it in a database with the user's information and the creation and expiration date (if we want it to be valid for a limited period of time).

How do you refresh an expired token?

The member must reauthorize your application when refresh tokens expire. When you use a refresh token to generate a new access token, the lifespan or Time To Live (TTL) of the refresh token remains the same as specified in the initial OAuth flow (365 days), and the new access token has a new TTL of 60 days.

How do I trigger a refresh token?

To use the refresh token, make a POST request to the service's token endpoint with grant_type=refresh_token , and include the refresh token as well as the client credentials if required.


3 Answers

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.

like image 91
Léon Logli Avatar answered Sep 21 '22 05:09

Léon Logli


Update - Jan 2022 you can see basic React JWT Authentication Setup from: https://github.com/earthguestg/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
});
like image 44
earthguestg Avatar answered Sep 22 '22 05:09

earthguestg


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.
like image 31
Marc Simon Avatar answered Sep 19 '22 05:09

Marc Simon