logaretm / villus

🏎 A tiny and fast GraphQL client for Vue.js

Home Page:https://villus.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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
});