Search code examples
reactjstypescriptaxiosauth0

access token from auth0provider outside of react components


I'm using the auth0 token provided by the user on login to make api calls via useAuth0.getTokenSilently.

In this example, fetchTodoList, addTodoItem, and updateTodoItem all require a token for authorization. I'd like to be able to extract these functions out in to a separate file (like utils/api-client.js and import them without having to explicitly pass in the token.

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

Here's a link to the repo: https://github.com/gpspake/todo-client


Solution

  • Ok, got it!

    Now that I understand better, my real question was how to provide an auth0 token to axios requests such that they don't need to be declared in components.

    The short answer: Get the token when auth0 is initialized and register an axios interceptor to set that token as a header value for all axios requests.

    The long answer (examples in typescript):

    Declare a function that takes a token and registers an 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
      })
    }
    
    

    In the auth0provider wrapper, when the auth0 client is initialized and authenticated, get the token with getTokenSilently and pass it to the setAxiosTokenInterceptor (Modified example from the 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()
    }, [])
    

    Calling setLoading(false) when the promise is resolved ensures that, if auth0 is finished loading, the interceptor has been registered. Since none of the components that make requests are rendered until auth0 is finished loading, this prevents any calls from being made without the token.

    This allowed me to move all of my axios functions in to a separate file and import them in to the components need them. When any of these functions are called, the interceptor will add the token to the header 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