Manuel Morales
Manuel Morales

Reputation: 106

Use Auth0's hook useAuth0 to get token and set header in Apollo client

Hello I'm trying to use Auth0's spa example with react and I'm using the useAuth0 hook, I'm also using Apollo client to make my queries and I need to get the token and set it in the request, however I haven't been able to set it.

I would be very grateful if someone could point me in the right direction.

I've tried to use the context property in the query/mutation component but I couldn't figure it out nor find information about how to use it.

Upvotes: 7

Views: 5117

Answers (4)

atb00ker
atb00ker

Reputation: 1105

So, I am using the @auth0/auth0-react and @apollo/client and I have managed to make it work as follows:

My app index.tsx:

<AuthProvider>
  <CustomApolloProvider>
    <Router>
      <MY_ROUTES>
    </Router>
  </CustomApolloProvider>
</AuthProvider>

Note: AuthProvider is just a alias for Auth0Provider for the purposes of this answer.

In CustomApolloProvider I have the following:

  1. Imports:
import React, { useEffect, useState } from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache, HttpLink } from '@apollo/client';
import { useAuth0 } from '@auth0/auth0-react';
  1. Get auth context with useAuth0 and create client state:
  const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();
  const [client, setClient] = useState(undefined as unknown as ApolloClient<any>)
  1. Trigger setClient when auth0 is ready:
  useEffect(() => {
    if (!isLoading && isAuthenticated) {
      // Here createApolloClient is a function that takes token as input
      // and returns `ApolloClient` instance. 
      getIdTokenClaims().then(jwtToken => setClient(createApolloClient(jwtToken.__raw)));
    }
  }, [isAuthenticated, isLoading]);
  1. Load page when client is available:
  if (!client)
    return <PageLoader />
  return (
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  );

A working example can be found on GitHub: https://github.com/atb00ker/ideation-portal/tree/1c6cbb26bb41f5a7b13a5796efd98bf1d77544cd/src/views

Upvotes: 0

Mel Macaluso
Mel Macaluso

Reputation: 3750

The way I tackled this issue is by editing an article I found online from https://hasura.io/

In other words, it uses react's useContext() hook and useEffect() to check and get the jwt token by using auth0's getTokenSilently() function.

I will just write the parts that are relevant:

import React, { FC, ReactNode } from 'react'
import { useAuth0 } from '@auth0/auth0-react'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient, HttpLink, InMemoryCache } from 'apollo-boost'
import { setContext } from 'apollo-link-context'
import { useState, useEffect } from 'react'

const httpLink = new HttpLink({
  uri: 'yourdomain.test/graphql',
})

const Page: FC<{}> = ({children }) => {
  const [accessToken, setAccessToken] = useState('')
  const [client, setClient] = useState() as [ApolloClient<any>, any] // that could be better, actually if you have suggestions they are welcome
  const { getAccessTokenSilently, isLoading } = useAuth0()

  // get access token
  useEffect(() => {
    const getAccessToken = async () => {
      try {
        const token = await getAccessTokenSilently()
        setAccessToken(token)
      } catch (e) {
        console.log(e)
      }
    }
    getAccessToken()
  }, [])

  useEffect(() => {
    const authLink = setContext((_, { headers }) => {
      const token = accessToken
      if (token) {
        return {
          headers: {
            ...headers,
            authorization: `Bearer ${token}`,
          },
        }
      } else {
        return {
          headers: {
            ...headers,
          },
        }
      }
    })

    const client = new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache(),
    })

    setClient(client)
  }, [accessToken])

  if (!client) {
    return <h1>Loading...</h1>
  }

  return (
    <ApolloProvider client={client}>
      {...children}
    </ApolloProvider>
  )
}

Upvotes: 3

Tanver Hasan
Tanver Hasan

Reputation: 1763

The main issue is that react hook can not be used outside function component. However, initialization of ApolloClient happens outside component and it requires access token to call backend graphql API which can be achieved by calling getTokenSilently() method. To solve this issue, I have exported the getTokenSilently() method manually (outside Auth0Provider).

For example:

import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";


const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
let _initOptions, _client

const getAuth0Client = () => {
  return new Promise(async (resolve, reject) => {
    let client
    if (!client)  {
      try {

        client = await createAuth0Client(_initOptions)
        resolve(client)
      } catch (e) {
        console.log(e);
        reject(new Error('getAuth0Client Error', e))
      }
    }
  })
}

export const getTokenSilently = async (...p) => {
  if(!_client) {
      _client = await getAuth0Client()
  }
  return await _client.getTokenSilently(...p);

}

export const Auth0Provider = ({
  children,
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  ...initOptions
}) => {
  const [isAuthenticated, setIsAuthenticated] = useState();
  const [user, setUser] = useState();
  const [auth0Client, setAuth0] = useState();
  const [loading, setLoading] = useState(true);
  const [popupOpen, setPopupOpen] = useState(false);

  useEffect(() => {
    const initAuth0 = async () => {
       _initOptions = initOptions;
       const client = await getAuth0Client(initOptions)
       setAuth0(client)
      // const auth0FromHook = await createAuth0Client(initOptions);
      // setAuth0(auth0FromHook);

      if (window.location.search.includes("code=")) {
        console.log("Found code")
        const { appState } = await client.handleRedirectCallback();
        onRedirectCallback(appState);
      }

      const isAuthenticated = await client.isAuthenticated();

      setIsAuthenticated(isAuthenticated);

      if (isAuthenticated) {
        const user = await client.getUser();
        setUser(user);
      }

      setLoading(false);
    };
    initAuth0();
    // eslint-disable-next-line
  }, []);

  const loginWithPopup = async (params = {}) => {
    setPopupOpen(true);
    try {
      await auth0Client.loginWithPopup(params);
    } catch (error) {
      console.error(error);
    } finally {
      setPopupOpen(false);
    }
    const user = await auth0Client.getUser();
    setUser(user);
    setIsAuthenticated(true);
  };

  const handleRedirectCallback = async () => {
    setLoading(true);
    await auth0Client.handleRedirectCallback();
    const user = await auth0Client.getUser();
    setLoading(false);
    setIsAuthenticated(true);
    setUser(user);
  };
  return (
    <Auth0Context.Provider
      value={{
        isAuthenticated,
        user,
        loading,
        popupOpen,
        loginWithPopup,
        handleRedirectCallback,
        getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
        loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
        getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
        getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
        logout: (...p) => auth0Client.logout(...p)
      }}
    >
      {children}
    </Auth0Context.Provider>
  );
};

Now, there is no restriction and we can call getTokenSilently() method either in function component or class component or any other place.

I have used following code to initialize ApolloClient and pass the client when calling ApolloProvider.

import React from "react";
import { Router, Route, Switch } from "react-router-dom";
import { Container } from "reactstrap";

import PrivateRoute from "./components/PrivateRoute";
import Loading from "./components/Loading";
import NavBar from "./components/NavBar";
import Footer from "./components/Footer";
import Home from "./views/Home";
import Profile from "./views/Profile";
import { useAuth0 } from "./react-auth0-spa";
import history from "./utils/history";
import "./App.css";
import { ApolloProvider } from '@apollo/react-hooks';
import initFontAwesome from "./utils/initFontAwesome";
import { InMemoryCache } from "apollo-boost";
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, Observable } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { withClientState } from 'apollo-link-state';
import {getTokenSilently} from "./react-auth0-spa";


initFontAwesome();


let API_URL="https://[BACKEND_GRAPHQL_API_URL]/graphql";

const cache = new InMemoryCache();
cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => {
  try {
    return cache.originalReadQuery(...args);
  } catch (err) {
    return undefined;
  }
};


const request = async (operation) => {
  const token = await getTokenSilently();
  operation.setContext({
    headers: {
      authorization: token ? `Bearer ${token}` : ''
    }
  });
};

const requestLink = new ApolloLink((operation, forward) =>
  new Observable(observer => {
    let handle;
    Promise.resolve(operation)
      .then(oper => request(oper))
      .then(() => {
        handle = forward(operation).subscribe({
          next: observer.next.bind(observer),
          error: observer.error.bind(observer),
          complete: observer.complete.bind(observer),
        });
      })
      .catch(observer.error.bind(observer));

    return () => {
      if (handle) handle.unsubscribe();
    };
  })
);



const client = new ApolloClient({
  link: ApolloLink.from([
    onError(({ graphQLErrors, networkError }) => {
      if (graphQLErrors) {
        console.log("Graphqlerrors"+graphQLErrors)
       // sendToLoggingService(graphQLErrors);
      }
      if (networkError) {
        console.log("Network error"+networkError)
       // logoutUser();
      }
    }),
    requestLink,
    withClientState({
      defaults: {
        isConnected: true
      },
      resolvers: {
        Mutation: {
          updateNetworkStatus: (_, { isConnected }, { cache }) => {
            cache.writeData({ data: { isConnected }});
            return null;
          }
        }
      },
      cache
    }),
    new HttpLink({
      uri: API_URL,
    // credentials: 'include'
    })
  ]),
  cache
});

const App = () => {
  const { loading } = useAuth0();

  if (loading) {
    return <Loading />;
  }

  return (
    <ApolloProvider client={client}>
    <Router history={history}>
      <div id="app" className="d-flex flex-column h-100">
        <NavBar />
        <Container className="flex-grow-1 mt-5">
          <Switch>
            <Route path="/" exact component={Home} />
            <PrivateRoute path="/profile" component={Profile} />
          </Switch>
        </Container>
        <Footer />
      </div>
    </Router>
    </ApolloProvider>
  );
};

export default App;

Upvotes: 1

Matt Wilson
Matt Wilson

Reputation: 8309

I had the same dilemma, especially since the Auth0 hook can only be used from within a functional component but the docs seem to set up the ApolloProvider in the index file.

With a bit of experimentation I managed to get around this by creating a wrapper component which allows me to make use of the useAuth0 hook and asynchronously fetch/attach the token to each request.

I created a new file AuthorizedApolloProvider.tsx:

import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/link-context';
import React from 'react';

import { useAuth0 } from '../react-auth0-spa';

const AuthorizedApolloProvider = ({ children }) => {
  const { getTokenSilently } = useAuth0();

  const httpLink = createHttpLink({
    uri: 'http://localhost:4000/graphql', // your URI here...
  });

  const authLink = setContext(async () => {
    const token = await getTokenSilently();
    return {
      headers: {
        Authorization: `Bearer ${token}`
      }
    };
  });

  const apolloClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
    connectToDevTools: true
  });

  return (
    <ApolloProvider client={apolloClient}>
      {children}
    </ApolloProvider>
  );
};

export default AuthorizedApolloProvider;

Then in my index.tsx file I wrap App with my new AuthorizedApolloProvider instead of using ApolloProvider directly.

ReactDOM.render(
  <Auth0Provider
    domain={config.domain}
    client_id={config.clientId}
    redirect_uri={window.location.origin}
    audience={config.audience}
    onRedirectCallback={onRedirectCallback}>

      <AuthorizedApolloProvider>
        <App />
      </AuthorizedApolloProvider>

  </Auth0Provider>,  
  document.getElementById('root')
);

Note: The above example is using Apollo Client 3 beta, and I had to install @apollo/link-context in addition to @apollo/client. I guess the required imports might be different for versions of Apollo Client.

Upvotes: 8

Related Questions