Refreshing access and refresh tokens via Apollo in React

I am trying to implement the logic for working with refresh and access tokens through Apollo in React. In the process of writing the code, I ran into the following problems.

  1. If the server (Graphql) returns me an error (access token expired), then it is intercepted by ErrorLink. And I have no idea how I can make a GraphQl mutation inside it to receive new tokens.

  2. Even if I receive new tokens, I understand that I have no idea how to send the original request again. mutation to receive new tokens.

And I have not found an example of such functionality in the documentation.

This is applicable to apollo/client v3.4.16

Instantiate the apollo client using link from to compose the request logic:

new ApolloClient({
  link: from([authLink, errorLink, httpLink]),
});

Using setContext api, define authLink like this:

const authLink = setContext(async (_, { headers }) => {
  const token = await getToken();
  return {
    headers: {
      ...headers,
      authorization: token,
    },
  };
});

Where getToken fn contains the logic to retrieve the token which I believe you already have.

@blackfry , what is the difference between the snippet you provided and the following one?

const authLink = new ApolloLink((operation, forward) => {
  const accessToken = localStorage.getItem("accessToken");

  operation.setContext(({ headers }) => ({
    headers: {
      ...headers,
      authorization: accessToken ? `Bearer ${accessToken}` : "",
    },
  }));

  return forward(operation);
});

consider that I read the documentation you provided but I could not solve how to refresh access and refresh token when the access token is expired… I also checked on SO but they implement an errorLink and some of the utility functions are imported and so I do not know what they do (like for example del getToken() of your example)

Hi @francesca_gia,

What is the difference between our code:
You are creating a singular ApolloLink instance which is fine if you do not need additional custom links. In my example I have an additional error handling link.

How to refresh the token:
the async function getToken is an http request to your authorisation endpoint that returns a new token if an existing token is not present or is expired.

const getToken = async (id) => {
  // check that stored token exists (localStorage perhaps) and whether it is expired
  // if it doesn't exist or is expired fetch a new one as below or return the valid existing token

  const response = await fetch("https://serverurl", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: { id },
  });
  return response.json();
};

I hope that helps

Hi, unfortunately I need some more explanations, if you could be so kind.
First, as a general question, what is the difference between defining the authLink as newApolloLink and setContext, that is between this two snippets:

const authLink = new ApolloLink((operation, forward) => {
  const accessToken = localStorage.getItem("accessToken");

  operation.setContext(({ headers }) => ({
    headers: {
      ...headers,
      authorization: accessToken ? `Bearer ${accessToken}` : "",
    },
  }));

  return forward(operation);
});

vs

const authLink = setContext((_, { headers }) => {
 const accessToken = localStorage.getItem("accessToken");
  return {
    headers: {
      ...headers,
      authorization: token,
    },
  };
});

Second, I am in a situation where, when the access token has expired I use the refresh token to ask for a new access token (and refresh token). So this is my implementation of the authLink, errorLink and getNewToken and it would be very helpful you could tell me something since at the moment it is not working :frowning:

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("accessToken");
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const getNewToken = async () => {
  try {
    const { data } = await axios.post(
      "https://fxxxx.ngrok.io/api/v2/refresh",
      { token: localStorage.getItem("refreshToken") }
    );
    localStorage.setItem("refreshToken", data.refresh_token);
    return data.access_token;
  } catch (error) {
    console.log(error);
  }
};

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) => {
                return;
              })
            )
              .filter((value) => Boolean(value))
              .flatMap((accessToken) => {
                const oldHeaders = operation.getContext().headers;
                operation.setContext({
                  headers: {
                    ...oldHeaders,
                    authorization: `Bearer ${accessToken}`,
                  },
                });
                return forward(operation);
              });
        }
      }
    }
  }
);

const client = new ApolloClient({
  link: from([authLink, errorLink, httpLink]),
  cache: new InMemoryCache(),
});