在组件之外从auth0provider访问令牌

时间:2020-05-18 13:12:33

标签: reactjs typescript axios auth0

我正在使用用户登录时提供的auth0令牌通过useAuth0.getTokenSilently进行api调用。

在此示例中,fetchTodoListaddTodoItemupdateTodoItem都需要令牌进行授权。我希望能够将这些函数提取到一个单独的文件中(例如utils/api-client.js并导入它们,而不必显式传递令牌。

import React, { useContext } from 'react'
import { Link, useParams } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircle, faList } from '@fortawesome/free-solid-svg-icons'
import axios from 'axios'
import { queryCache, useMutation, useQuery } from 'react-query'
import { TodoItem } from '../models/TodoItem'
import { TodoInput } from './TodoInput'
import { TodoList as TodoListComponent } from './TodoList'
import { TodoListsContext } from '../store/todolists'
import { TodoListName } from './TodoListName'
import { TodoList } from '../models/TodoList'
import { useAuth0 } from '../utils/react-auth0-wrapper'

export const EditTodoList = () => {

  const { getTokenSilently } = useAuth0()

  const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.get(
        `/api/TodoLists/${todoListId}`,
        {
          headers: {
            Authorization: `Bearer ${token}`
          }
        }
      )
      return data
    } catch (error) {
      return error
    }
  }

  const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.post(
        '/api/TodoItems',
        todoItem,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          }
        }
      )
      return data
    } catch (addTodoListError) {
      return addTodoListError
    }
  }

  const updateTodoItem = async (todoItem: TodoItem) => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.put(
        '/api/TodoItems',
        todoItem,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          }
        }
      )
      return data
    } catch (addTodoListError) {
      return addTodoListError
    }
  }

  const [updateTodoItemMutation] = useMutation(updateTodoItem, {
    onSuccess: () => {
      queryCache.refetchQueries(['todoList', todoListId])
    }
  })

  const [addTodoItemMutation] = useMutation(addTodoItem, {
    onSuccess: () => {
      console.log('success')
      queryCache.refetchQueries(['todoList', todoListId])
    }
  })

  const onAddTodoItem = async (todoItem: TodoItem) => {
    try {
      await addTodoItemMutation({ 
        ...todoItem, 
        todoListId: parseInt(todoListId, 10) 
      })
    } catch (error) {
      // Uh oh, something went wrong
    }
  }

  const { todoListId } = useParams()
  const { status, data: todoList, error } = useQuery(['todoList', todoListId], () => fetchTodoList(todoListId))
  const { todoLists, setTodoList } = useContext(TodoListsContext)
  const todoListIndex = todoLists.findIndex(
    list => todoListId === list.id.toString()
  )

  const setTodoItems = (todoItems: TodoItem[]) => {
    // if(todoList) {
    //   const list = { ...todoList, todoItems }
    //   setTodoList(todoListIndex, list)
    // }
  }

  const setTodoListName = (name: string) => {
    // setTodoList(todoListIndex, { ...todoList, name })
  }

  return (
    <>
      <Link className="block flex align-items-center mt-8" to="/">
        <span className="fa-layers fa-fw fa-3x block m-auto group">
          <FontAwesomeIcon 
            icon={faCircle} 
            className="text-teal-500 transition-all duration-200 ease-in-out group-hover:text-teal-600" 
          />
          <FontAwesomeIcon icon={faList} inverse transform="shrink-8" />
        </span>
      </Link>

      {status === 'success' && !!todoList && (
        <>
          <TodoListName
            todoListName={todoList.name}
            setTodoListName={setTodoListName}
          />
          <TodoInput 
            onAddTodoItem={onAddTodoItem} 
          />

          <TodoListComponent
            todoItems={todoList.todoItems}
            setTodoItems={setTodoItems}
            updateTodo={updateTodoItemMutation}
          />
        </>
      )}
    </>
  )
}

以下是回购链接:https://github.com/gpspake/todo-client

5 个答案:

答案 0 :(得分:2)

我不确定您为什么不能在各个函数内部访问令牌?是不是因为它们不是React函数组件而是常规函数?

我要做的一件事情是创建一个useFetch挂钩,该挂钩可以获取用户令牌并将其附加到请求本身。然后,无需专门导出这些功能,我可以调用此新的fetch挂钩。这是我的意思的例子。

import React from "react"
import { useAuth0 } from "../utils/auth"

const useFetch = () => {
  const [response, setResponse] = React.useState(null)
  const [error, setError] = React.useState(null)
  const [isLoading, setIsLoading] = React.useState(false)
  const { getTokenSilently } = useAuth0()

  const fetchData = async (url, method, body, authenticated, options = {}) => {
    setIsLoading(true)
    try {
      if (authenticated) {
        const token = await getTokenSilently()
        if (!options.headers) {
          options.headers = {}
        }
        options.headers["Authorization"] = `Bearer ${token}`
      }
      options.method = method
      if (method !== "GET") {
        options.body = JSON.stringify(body)
      }
      const res = await fetch(url, options)
      const json = await res.json()
      setResponse(json)
      setIsLoading(false)
      if (res.status === 200) {
        return json
      }
      throw { msg: json.msg }
    } catch (error) {
      console.error(error)
      setError(error)
      throw error
    }
  }
  return { response, error, isLoading, fetchData }
}

export default useFetch

答案 1 :(得分:2)

这是@james-quick 答案的变体,我使用“RequestFactory”生成 axios 格式的请求,然后只添加来自 Auth0 的 auth 标头

我遇到了同样的问题,我通过将所有 API 调用逻辑移到我创建的自定义钩子中来解决此限制:

import { useAuth0 } from '@auth0/auth0-react';
import { useCallback } from 'react';
import makeRequest from './axios';

export const useRequest = () => {
  const { getAccessTokenSilently } = useAuth0();

  // memoized the function, as otherwise if the hook is used inside a useEffect, it will lead to an infinite loop
  const memoizedFn = useCallback(
    async (request) => {
      const accessToken = await getAccessTokenSilently({ audience: AUDIANCE })
      return makeRequest({
        ...request,
        headers: {
          ...request.headers,
          // Add the Authorization header to the existing headers
          Authorization: `Bearer ${accessToken}`,
        },
      });
    },
    [isAuthenticated, getAccessTokenSilently]
  );
  return {
    requestMaker: memoizedFn,
  };
};

export default useRequest;

用法示例:

 import { RequestFactory } from 'api/requestFactory';

 const MyAwesomeComponent = () => {
   const { requestMaker } = useRequest(); // Custom Hook
   ...
   requestMaker(QueueRequestFactory.create(queueName))
     .then((response) => {
       // Handle response here
       ...
     });
 }

RequestFactory 为我的不同 API 调用定义并生成请求负载,例如:

export const create = (queueName) => ({ method: 'post', url: '/queue', data: { queueName } });

Here 是完整的 Auth0 集成 PR,供参考。

答案 2 :(得分:1)

有多种解决方法。

不要过多更改代码库。我会去一个带有提供者挂钩商店。那里有很多商店图书馆。

这是一个很小的版本,也可以在React渲染之外使用。
https://github.com/storeon/storeon

这只是一个很小的商店的例子,我发现这可能符合要求。

在React外部使用商店库可能看起来像这样:

import store from './path/to/my/store.js;'

// Read data
const state = store.get();

// Save data in the store
store.dispatch('foo/bar', myToken);

答案 3 :(得分:1)

我在如何在 React 组件之外使用 getAccessTokenSilently 时遇到了类似的问题,我最终得到的是:

我的 HTTP 客户端包装器

export class HttpClient {
  constructor() {
    HttpClient.instance = axios.create({ baseURL: process.env.API_BASE_URL });

    HttpClient.instance.interceptors.request.use(
      async config => {
        const token = await this.getToken();

        return {
          ...config,
          headers: { ...config.headers, Authorization: `Bearer ${token}` },
        };
      },
      error => {
        Promise.reject(error);
      },
    );

    return this;
  }

  setTokenGenerator(tokenGenerator) {
    this.tokenGenerator = tokenGenerator;
    return this;
  }

  getToken() {
    return this.tokenGenerator();
  }
}


在我的应用根目录中,我从 auth0 传递了 getAccessTokenSilently

 useEffect(() => {
    httpClient.setTokenGenerator(getAccessTokenSilently);
  }, [getAccessTokenSilently]);

就是这样!

您现在有一个 axios 实例准备好使用

进行身份验证的请求

答案 4 :(得分:0)

好,知道了!

现在我更了解了,我真正的问题是如何为axios请求提供auth0令牌,这样就无需在组件中声明它们。

简短答案: 初始化auth0时获取令牌,然后注册axios interceptor将该令牌设置为所有axios请求的标头值。

长答案(打字稿中的示例):

声明一个带有令牌并注册axios interceptor

的函数
const setAxiosTokenInterceptor = async (accessToken: string): Promise<void> => {
  axios.interceptors.request.use(async config => {
    const requestConfig = config
    if (accessToken) {
      requestConfig.headers.common.Authorization = `Bearer ${accessToken}`
    } 
    return requestConfig
  })
}

在auth0provider包装器中,当auth0客户端被初始化和认证时,使用setAxiosTokenInterceptor获取令牌,并将其传递给注册拦截器的函数(来自Auth0 React SDK Quickstart的修改示例):

useEffect(() => {
    const initAuth0 = async () => {
        const auth0FromHook = await createAuth0Client(initOptions)
        setAuth0(auth0FromHook)

        if (window.location.search.includes('code=')) {
            const { appState } = await auth0FromHook.handleRedirectCallback()
            onRedirectCallback(appState)
        }

        auth0FromHook.isAuthenticated().then(
            async authenticated => {
                setIsAuthenticated(authenticated)
                if (authenticated) {
                    auth0FromHook.getUser().then(
                        auth0User => {
                            setUser(auth0User)
                        }
                    )
                    // get token and register interceptor
                    const token = await auth0FromHook.getTokenSilently()
                    setAxiosTokenInterceptor(token).then(
                        () => {setLoading(false)}
                    )
                }
            }
        )


    }
    initAuth0().catch()
}, [])

在解决承诺后调用setLoading(false)可以确保,如果auth0完成加载,则拦截器已经注册。由于在auth0完成加载之前,不会呈现任何发出请求的组件,因此可以防止在没有令牌的情况下进行任何调用。

这使我可以将所有axios函数移到一个单独的文件中,并将它们导入需要它们的组件中。当调用这些函数中的任何一个时,拦截器会将令牌添加到标头中 utils/todo-client.ts


import axios from 'axios'
import { TodoList } from '../models/TodoList'
import { TodoItem } from '../models/TodoItem'

export const fetchTodoLists = async (): Promise<TodoList[]> => {
  try {
    const { data } = await axios.get(
      '/api/TodoLists'
    )
    return data
  } catch (error) {
    return error
  }
}

export const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
  try {
    const { data } = await axios.get(
      `/api/TodoLists/${todoListId}`
    )
    return data
  } catch (error) {
    return error
  }
}

export const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
  try {
    const { data } = await axios.post(
      '/api/TodoItems',
      todoItem
    )
    return data
  } catch (addTodoListError) {
    return addTodoListError
  }
}
...

Full source on github