How to use with JWT/RefreshToken? (and implement a retry strat)
OClement opened this issue · comments
sorry this is not an issue per se; but I couldn't find a satisfying answer anywhere else
Is there a proven strategy for the following?
- User calls the api using a JWT auth
- Authorizer rejects the token (eg: expired) and returns 401
- App calls a refresh token api and renew the JWT
- App re-execute the query that originally failed transparently
I've been toying with the Plugin API and could get up to the refresh part, but there doesn't seem to be a way to centralize the "retry" strategy in this case.
We're to the point where we are wrapping useQuery/useMutation to handle this in 1 place, but I was wondering if there's a better way? 🤔
Thanks
Okay so at the moment I can't think of a solid implementation to show how we might go about implementing this. But quickly off the top of my head I can think of the following.
The plugin API could make this work but you must implement a custom fetch plugin here since you want to avoid setting the GQL result before retrying a couple of times and after you've successfully generated the token.
So starting off with the same codebase as the fetch plugin:
import { GraphQLError } from 'graphql';
// getActiveClient() isn't available yet but you can store the `Client` instance from `useClient` or `createClient` globally somewhere until #156 is merged
// you will also probably need some utils from `villus` to be exposed which we can expose in a separate PR
import { getActiveClient } from 'villus';
import { RefreshToken } from '@/graphql/Auth.gql'
// this is to make sure we don't duplicate `RefreshToken` calls
let refreshPromise = null;
export function fetchWithRetry(opts) {
// same lines from 11 to 33
// https://github.com/logaretm/villus/blob/main/packages/villus/src/fetch.ts#L11-L33
// Change this to however you are going to figure out if the token is expired
// maybe you can check the payload error message
if (response.status === 401 && !refreshPromise) {
client = getActiveClient();
refreshPromise = client.executeMutation({ query: RefreshToken }).then(data => {
// store the new token or auth data somewhere and set it on the fetchOpts
setTimeout(() => {
// clear up the promise, you don't want to do this immediately so toy around with the timeout to test it
// maybe there is a more reliable way to clear it, but can't think of one at the moment
refreshPromise = null;
}, 100)
});
}
if (response.status === 401 && refreshPromise) {
await refreshPromise;
// re-execute the same code from lines 17 to 31
// https://github.com/logaretm/villus/blob/main/packages/villus/src/fetch.ts#L17-L31
}
// same rest of the fetch function
}
This could need some clean up and some bug fixing. Anyways I think this is a good example worth adding to the docs I think, I will try to work on that on the weekend.
We will need something like global error events. E.g. when creating a client, use the function useGlobalError and in it give a callback with the query used e.g. refetch()
const onError = ((error, refetch) => {
if(error.code === 'UNAUTHORIZED') {
// revoke token with refreshToken
refetch();
}
});
const client = createClient({
url: '/graphql', // your endpoint.,
errorHandle: onError
});