3

I am using urql GraphQL client in my project. After I log in, it's not adding a token in the authorization header automatically, as it should. I have to refresh the page once and then the token is set in the header. I have been through the documentation and can't seem to figure out what I am doing wrong.

Here is my implementation :

export const urqlClient = createClient({
  url: `${url}`,
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      getAuth: async ({ authState, mutate }: any) => {

        // for initial launch, fetch the auth state from storage (local storage, async storage etc)
        const token = Cookies.get('token');
        if (!authState) {
          if (token) {
            return { token };
          }
          return null;
        }

        // refresh token
        const decoded = jwt_decode<MyToken>(token as any);
        if (typeof decoded.exp !== 'undefined' && decoded.exp < Date.now() / 1000) {
          const user = firebase.auth().currentUser;
          if (user) {
            await user.getIdToken();
          }
        }

        //auth has gone wrong. Clean up and redirect to a login page
        Cookies.remove('token');
        await signOut();
      },
      addAuthToOperation: ({ authState, operation }: any) => {
        // the token isn't in the auth state, return the operation without changes
 
        if (!authState || !authState.token) {
          return operation;
        }

        // fetchOptions can be a function (See Client API) but you can simplify this based on usage
        const fetchOptions =
          typeof operation.context.fetchOptions === 'function'
            ? operation.context.fetchOptions()
            : operation.context.fetchOptions || {};

        return makeOperation(operation.kind, operation, {
          ...operation.context,
          fetchOptions: {
            ...fetchOptions,
            headers: {
              ...fetchOptions.headers,
              Authorization: `Bearer ${authState.token}`,
            },
          },
        });
      },
    }),
    fetchExchange,
  ]
});

What am I doing wrong here? Thanks

Thor0o0
  • 367
  • 1
  • 4
  • 17

3 Answers3

3

You haven't configured any signal for the authExchange to use for triggering the getAuth function again. What's crucial to understand about this is that the getAuth function on triggers in three different cases:

  • on startup
  • when willAuthError returns true before an operation
  • when didAuthError returns true after a result

So you have two options here; either you need to make sure that willAuthError or didAuthError can recognise that you've logged in, which would be automatic if your next operation returns an auth error. The docs document how to set this option up to do so.

The alternative is to return a mutable object in getAuth that you can update outside of the getAuth function, either to update it or to ensure that willAuthError triggers

Phil Plückthun
  • 1,381
  • 9
  • 14
2

You're missing

credentials: 'include'

headers: {
              ...fetchOptions.headers,
              Authorization: `Bearer ${authState.token}`,
            },
credentials: 'include', // missing this
Jonathan Laliberte
  • 2,672
  • 4
  • 19
  • 44
0

I was going around in circles with this one and was about to roll back to Apollo Client, but ended up having success by returning true to willAuthError if token was not set in local state or if token was not set in authState. For me my local state is Redux, but this could be localStorage or cookie.

const willAuthError: AuthConfig<AuthState>['willAuthError'] = ({
  authState,
}) => {
  const reduxToken = store.getState().auth.token;
  const authStateToken = authState?.token;

  // if true is returned then `getAuth` will be re-executed updating `authState`
  // so execute `getAuth` if token is not set in redux store or token not set in `authState`
  return !reduxToken || !authStateToken;
};

For reference my full urql client is as follows (note that this is a React Native project, so token is persisted between sessions in expo-secure-store

import { AuthConfig, authExchange } from '@urql/exchange-auth';
import * as SecureStore from 'expo-secure-store';
import {
  cacheExchange,
  CombinedError,
  createClient,
  dedupExchange,
  errorExchange,
  fetchExchange,
  makeOperation,
  Operation,
} from 'urql';

import { store } from '@/redux/store';
import { SecureStorageKeys } from '@/typescript/enums';

import Config from '../Config';

interface AuthState {
  token?: string;
}

const getAuth: AuthConfig<AuthState>['getAuth'] = async ({ authState }) => {
  if (!authState) {
    const token = await SecureStore.getItemAsync(SecureStorageKeys.AUTH_TOKEN);

    if (token) {
      return { token };
    }

    return null;
  }

  return null;
};

const addAuthToOperation: AuthConfig<AuthState>['addAuthToOperation'] = ({
  authState,
  operation,
}) => {
  if (!authState || !authState.token) {
    return operation;
  }

  const fetchOptions =
    typeof operation.context.fetchOptions === 'function'
      ? operation.context.fetchOptions()
      : operation.context.fetchOptions || {};

  return makeOperation(operation.kind, operation, {
    ...operation.context,
    fetchOptions: {
      ...fetchOptions,
      headers: {
        ...fetchOptions.headers,
        authorization: authState.token ? `Bearer ${authState.token}` : '',
      },
    },
  });
};

const willAuthError: AuthConfig<AuthState>['willAuthError'] = ({
  authState,
}) => {
  const reduxToken = store.getState().auth.token;
  const authStateToken = authState?.token;

  // if true is returned then `getAuth` will be re-executed updating `authState`
  // so execute `getAuth` if token is not set in redux store or token not set in `authState`
  return !reduxToken || !authStateToken;
};

const onError = (
  { graphQLErrors, networkError }: CombinedError,
  _operation: Operation
) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) => {
      return console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      );
    });

  if (networkError) console.log(`[Network error]: ${networkError}`);
};

const client = createClient({
  url: Config.apiUrl,
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      getAuth,
      addAuthToOperation,
      willAuthError,
    }),
    errorExchange({ onError }),
    fetchExchange,
  ],
});

export default client;
oldo.nicho
  • 2,149
  • 2
  • 25
  • 39