使用Auth0的钩子useAuth0来获取令牌并在Apollo客户端中设置标头

时间:2019-08-07 17:31:57

标签: reactjs graphql apollo react-hooks auth0

你好,我正在尝试将Auth0的spa示例与react一起使用,并且正在使用useAuth0钩子,也正在使用Apollo客户端进行查询,但是我需要获取令牌并在请求中进行设置,但是我无法设置

如果有人能为我指出正确的前进方向,我将不胜感激

我试图在查询/变异组件中使用context属性,但是我无法弄清楚它,也找不到有关如何使用它的信息。

4 个答案:

答案 0 :(得分:3)

我遇到了同样的难题,尤其是因为Auth0挂钩只能在功能组件中使用,但是文档似乎在索引文件中设置了ApolloProvider。

通过一些实验,我设法创建了一个包装器组件来解决此问题,该组件使我可以利用useAuth0钩子并将令牌异步地获取/附加到每个请求。

我创建了一个新文件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;

然后在我的index.tsx文件中,用新的App包装AuthorizedApolloProvider而不是直接使用ApolloProvider

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

注意:上面的示例使用的是Apollo Client 3 beta,除了@apollo/link-context之外,我还必须安装@apollo/client。我想所需的导入对于Apollo Client的版本可能会有所不同。

答案 1 :(得分:1)

主要问题是react挂钩不能在功能组件外部使用。但是,ApolloClient的初始化发生在组件外部,并且需要访问令牌才能调用后端graphql API,这可以通过调用getTokenSilently()方法来实现。为了解决此问题,我已经手动导出了getTokenSilently()方法(在Auth0Provider外部)。

例如:

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

现在,没有任何限制,我们可以在函数组件或类组件中或任何其他位置调用getTokenSilently()方法。

我已经使用以下代码初始化ApolloClient并在调用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;

答案 2 :(得分:0)

我解决此问题的方法是编辑我从https://hasura.io/网上找到的一篇文章

换句话说,它通过使用auth0的useContext()函数,使用react的useEffect()钩子和getTokenSilently()来检查并获取jwt令牌。

我将只写相关的部分:

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

答案 3 :(得分:0)

因此,我正在使用 @auth0/auth0-react@apollo/client,并且已设法使其工作如下:

我的应用index.tsx

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

注意:就本回答而言,AuthProvider 只是 Auth0Provider 的别名。

CustomApolloProvider 中,我有以下内容:

  1. 进口:
import React, { useEffect, useState } from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache, HttpLink } from '@apollo/client';
import { useAuth0 } from '@auth0/auth0-react';
  1. 使用 useAuth0 获取身份验证上下文并创建客户端状态:
  const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();
  const [client, setClient] = useState(undefined as unknown as ApolloClient<any>)
  1. 在 auth0 准备好时触发 setClient
  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. 当客户端可用时加载页面:
  if (!client)
    return <PageLoader />
  return (
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  );

可以在 GitHub 上找到一个工作示例:https://github.com/atb00ker/ideation-portal/tree/1c6cbb26bb41f5a7b13a5796efd98bf1d77544cd/src/views