【问题标题】:Use Auth0's hook useAuth0 to get token and set header in Apollo client使用 Auth0 的钩子 useAuth0 在 Apollo 客户端中获取令牌并设置标头
【发布时间】:2019-12-15 09:32:38
【问题描述】:

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

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

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

【问题讨论】:

    标签: reactjs graphql apollo react-hooks auth0


    【解决方案1】:

    所以,我正在使用@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

    【讨论】:

      【解决方案2】:

      我解决这个问题的方法是编辑我从https://hasura.io/在线找到的一篇文章

      也就是说,它使用 react 的 useContext() 钩子和 useEffect() 来通过 auth0 的 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】:

        主要问题是 react hook 不能在函数组件之外使用。但是,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;
        

        【讨论】:

          【解决方案4】:

          我也遇到了同样的困境,特别是因为 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 文件中,我用我的新 AuthorizedApolloProvider 包装 App,而不是直接使用 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/client 之外,我还必须安装@apollo/link-context。我想 Apollo Client 版本所需的导入可能会有所不同。

          【讨论】:

          • 刚刚在我的 react-app 中使用了该解决方案,如前所述,导入略有不同。我必须从 '@apollo/client/link/context';'import { setContext} 导入 setContect和 useAuth0 from 'import { useAuth0 } from '@auth0/auth0-react';'。像魅力一样工作:)
          • 使用上面的代码时,我注意到对 graphql 端点的调用发生了两次。 gist.github.com/abhi40308/… 引用自 hasura.io/blog/instagram-clone-react-graphql-hasura-part2 按预期工作
          • 之所以出现两次,是因为BrowserRouter放在了Authorized Apollo Provider之上。更改嵌套顺序解决了它。
          • 每次新渲染都创建客户端没有问题吗?
          猜你喜欢
          • 2021-01-15
          • 2020-03-25
          • 2021-08-13
          • 2019-12-31
          • 2021-02-09
          • 2021-04-04
          • 1970-01-01
          • 2018-03-05
          • 1970-01-01
          相关资源
          最近更新 更多