Martinocom
Martinocom

Reputation: 330

React Query uses outdated headers (old JWT token) after Keycloak refresh token

I'm pretty new in React-Native programming, but here is the context.

We are using React Query and Axios libraries in our project. As AuthManager we are using Keycloak and for the library managing auth status we have React Native Keycloak. We encounter a tedious problem with our server responding randomly 401 at our requests after a certain amount of time, bringing also to the app crash sometimes.

We reproduced the error making the Bearer Token of Keycloak expire after only 1 minute. This caused almost immediatly the 401 error and we wondered why this is happening.

Let's say we have a screen with some "Activities" and this screen is the first thing the user will see. For handling requests, in our code we use some custom hooks that reference useQuery, for example:

export function useActivities(): UseQueryResult<ActivityList> {
    const { headers } = useHeaders();

    return useQuery(
        ['activities', today.start],
        () => getActivitiesList(headers), // Note 1
        {
            enabled: !!today.start,
        }
    );
}

The important point of it is that we useHeaders to get our updated headers with the Keycloak token and our realm settings. useHeaders is almost everywhere in our app.

export function useHeaders(): UseHeaders {
    const { keycloak } = useKeycloak();
    const KEYCLOAK_REALM = remoteConfig().getString('KEYCLOAK_REALM');

    const headers = {
        Authorization: `Bearer ${keycloak?.token}`,
        Realm: KEYCLOAK_REALM,
    };

    return { headers };
}

Now, the getActivitiesList is simple as five:

async function getActivitiesList(headers: UseHeaders['headers']): Promise<ActivityList> {
    const url = `${BASE_URL}${VERSION}/activities/grouped?end=${end}&start=${start}`;

     // Note 2
    return axios
     .get(url, { headers })
     .then((res) => res.data)
     .catch((e) => console.error('Error fetching grouped activities:', e));
}

The problem with all of that is that whenever Keycloak will trigger the refresh token, the token inside keycloak object is changed, the headers inside useActivities are changed BUT if I print the headers inside my getActivitiesList (// Note 2), or even inside the query function (// Note 1), headers will not be updated. Sometimes it just causes to make two requests (one that fails and show error, the other one actually working), some other times the app crashes without any explain. This makes me wonder why the query function will not update its headers and passed the "old" headers inside getActivitiesList.

For now we are mitigating this problem in two different points.

  1. After keycloak init, we pass immediatly to axios a global header with axios.defaults.headers.common.Realm = KEYCLOAK_REALM;
  2. After receiving a valid token from Keycloak, we overwrite the Authorization header with a new one: axios.defaults.headers.common.Authorization = 'Bearer ${keycloak?.token}';

This is not a perfect solution and we are here to search some info about this problem.

Someone get something similar? How to manage the token refresh in useQuery function?

Upvotes: 2

Views: 1378

Answers (1)

Martinocom
Martinocom

Reputation: 330

After almost a year I can admit I was dumb. Actually the solution was pretty simple.

TL;DR

One cannot expect to get fresh value, if value is a const that doesn't change once rendered. But you can "fetch" a new token, requesting with a function, not just "initialized" as const.

// This is a function, not a simple const anymore
const headers = () => {
    Authorization: `Bearer ${keycloak?.token}`,
    Realm: KEYCLOAK_REALM,
};

// Usage, please note the "()" after headers, it's a function now!
axios.get(url, { headers() })

LONG ANSWER

When using useHeaders the React-Native render cycle will render the hook once. The hook itself will initialize the const headers and it will never change again, until a re-render will happen. But if the const headers will not change after timeout time, token will be invalid. If something will cause a re-render of that hook, the const headers will be reinitialized, so it will contain a fresh token.

This was the reason sometimes it would work, sometimes not. The solution is pretty simple. Since useKeycloak has always a valid and fresh token, instead of going with a simple variable declaration:

const headers = something

we can go with a

const headers = () => something

that will always return a new fresh (eventually refreshed) something, that in this case would be my precious token.

Upvotes: 0

Related Questions