Reputation: 106
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
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:
import React, { useEffect, useState } from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache, HttpLink } from '@apollo/client';
import { useAuth0 } from '@auth0/auth0-react';
useAuth0
and create client state: const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();
const [client, setClient] = useState(undefined as unknown as ApolloClient<any>)
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]);
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
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
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
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