auth0 / auth0-react

Auth0 SDK for React Single Page Applications (SPA)
MIT License
887 stars 255 forks source link

How to access the token within a redux middleware that is outside of the Auth0Provider context #67

Closed zachrickards closed 4 years ago

zachrickards commented 4 years ago

With redux I have a middleware that handles the request with errors and getting the token.

Example please?

majugurci commented 4 years ago

I have a small util for this.

index.js

<Auth0Provider
    domain="your-domain"
    clientId="your-client-id"
    redirectUri={window.location.origin}
>
    <App />
</Auth0Provider>

app.js

const App = () => {
    const {  getAccessTokenSilently } = useAuth0();
    sec.setAccessTokenSilently(getAccessTokenSilently);
}

security.js

let getAccessTokenSilently = null;

export const sec = {
    getAccessTokenSilently: () => getAccessTokenSilently,
    setAccessTokenSilently: (func) => (getAccessTokenSilently = func)
};

And then when I need it I call it like this:

const accessToken = await sec.getAccessTokenSilently()({
    audience: `your-audience`,
    scope: 'scopes'
});
cbeiro commented 4 years ago

Hi, is there a way of calling getAccessTokenSilently from outside a react component? I need to get the token in an axios interceptor.

majugurci commented 4 years ago

Hi, is there a way of calling getAccessTokenSilently from outside a react component? I need to get the token in an axios interceptor.

Hi, that's the purpose of this security.js util in my previous reply. You can use it anywhere in your code to obtain access token, regardless be it react component or plain js file.

For example, if you have api calls in a separate js file, let's call it apiTest.js.

apiTest.js

import { sec } from './security';

const fetch = async () => {
    const accessToken = await sec.getAccessTokenSilently()({
        audience: `your-audience`,
        scope: 'scopes'
    });

    console.log('accessToken', accessToken);
};

export const apiTest = {
    fetch
};
adamjmcgrath commented 4 years ago

Hi there,

Yep, as per @majugurci's example, the general pattern for accessing something in React Context outside of a component would be putting it in some global/module state

const MyContext = React.createContext({ someMethod: () => {} });

let myMethodThatCanBeExecutedFromAnywhere;

const App = () => {
  const { someMethod } = useContext(MyContext);
  myMethodThatCanBeExecutedFromAnywhere = someMethod;
  return <></>;
}

ReactDOM.render(<MyContext.Provider><App/></MyContext.Provider>, domEl);

With the caveat that this is fine for things that don't change over time (eg getAccessTokenSilently method) but not for things that do (eg isAuthenticated property)

Closing this, but feel free to reopen if you want to continue the conversation

plantshopping commented 3 years ago

Hey @adamjmcgrath, do you have a full sample of how to use React context with getAccessTokenSilently?

adamjmcgrath commented 3 years ago

Hi @plantshopping and all, I've created a guide as to why we don't offer the option to pass your own SPA JS client into the Provider and what solutions we suggest to takle these sorts of problems here https://gist.github.com/adamjmcgrath/0ed6a04047aad16506ca24d85f1b2a5c

plantshopping commented 3 years ago

Thanks @adamjmcgrath. Does this mean that whenever I want to make an API call I need to grab that instance of Axios Provider?

Currently I make all my api calls via a services file that doesn't require me to pass in an instance of Axios everytime:

// ItemsService.ts
import fetchClient from "src/axios";

export const getItems = async => {
    await fetchClient.get('/api');
}

export const postItem = async => {
    await fetchClient.post('/api');
}

// axios.ts
const fetchClient = axios.create();

fetchClient.interceptors.request.use(function (config) {
    // do something with config
    return config;
});

export default fetchClient;

// Items Component snippet
import getItems from "src/ItemsService";

export default function Items() {
    // Usage of getItems
    var items = await getItems();
}

If I follow the gist from the above, it feels like i would need to pass in Axios every time which would be a lot of code duplication.

adamjmcgrath commented 3 years ago

Hi @plantshopping - see the bottom example of the gist that shows you how to define a static getAccessTokenSilently if you don't want to do all this in React Context.

plantshopping commented 3 years ago

Hey @adamjmcgrath I tried to do the following:

ReactDOM.render(
  <React.StrictMode>
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      redirectUri={window.location.origin}
      audience={audience}
    >
      <BrowserRouter>
        <Auth0Context.Consumer>
          {() => {
            const { getAccessTokenSilently } = useAuth0();
            deferred.resolve(getAccessTokenSilently);
            return <App />;
          }}
        </Auth0Context.Consumer>
      </BrowserRouter>
    </Auth0Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

But I get the following error:

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
plantshopping commented 3 years ago

If I removed the hook const { getAccessTokenSilently } = useAuth0(); then I get the below error:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
    at Auth0Provider
adamjmcgrath commented 3 years ago

Hi @plantshopping - I've put a working example here https://codepen.io/adamjmcgrath/pen/WNOZNBP

plantshopping commented 3 years ago

@adamjmcgrath excellent! thanks very much

gpspake commented 3 years ago

First thanks to everyone in this thread; I'm glad I'm not the only one - sorry for commenting on a closed issue here 😬

I've read through and tried the examples above and I'm still having a hard time with this. I'm trying to refactor an app written before the react sdk to use auth0-react. I'm using Typescript.

The app is currently using @auth0/auth0-spa-js in an a react-auth0-wrapper. The trick is:

const token = await auth0FromHook.getTokenSilently()
setAxiosTokenInterceptor(token).then(
  () => {setLoading(false)}
)

Source Based on this codesandbox

That's worked great for me and I think this thread describes how I can basically do the same thing with the react-sdk but the examples aren't clicking for me or I might be running in to TS issues.

As a control, I created a fresh Create React App with Typescript and set up the Auth0 React Sdk, got that working and ported enough of the current app over to have requests fail because the token's not included. Here's that app

The reason I like the intereceptor approach is because I can put all of my axios fetch functions in a nice little client file knowing that when they get called, the interceptor will include my token.

As a sidenote, I happen pass those fetch functions to custom react-query hooks which are also just a bunch of functions in a file.

So I'd kinda like to keep that pattern but I'm open to suggestions, here. I think I'm pretty close to being able to use the examples in this file I'm just not quite there yet 😅

frederikprijck commented 3 years ago

Hey @gpspake ,

Could you elaborate on what you mean with thg token's not included in the sample app? From the looks of it, it is expected as there is nothing setting the header.

When using Axios in a React application that uses functional components, be sure to implement Axios in a way as described here: https://github.com/sheaivey/react-axios.

The idea would be to use some kind of axios provider, register your axios instance on the top level and add your interceptors. If you do it this way, and ensure the axios provider is a child of the Auth0Provider, you can call useAuth0 inside that Axios interceptor to add the token to every request.

A bit like explained here, but with Axios instead of Apollo: https://github.com/auth0/auth0-react/issues/266#issuecomment-908100980

OriAmir commented 2 years ago

hi @adamjmcgrath , I just wonder if what @majugurci offer as "global/module state" is the best pattern to access to the getAccessTokenSilently func or the correct way is what you suggest here: https://gist.github.com/adamjmcgrath/0ed6a04047aad16506ca24d85f1b2a5c because the code in that snippet looks a little wired and not really understood what happening here with the context and how we access the function outside of the component.

<Auth0Provider>
    {() => {
      const { getAccessTokenSilently } = useAuth0();
      const instance = axios.create()
      instance.interceptors.request.use(function () {/* use getAccessTokenSilently */});
      return (
        <AxiosProvider axios={instance}>
          <App />
        </AxiosProvider>)
    }}
  </Auth0Provider>
user4302 commented 1 year ago

import { sec } from './security';

following this, i get the token, but the function that calls this is stuck in await? im not sure how else to describe it.

AntonioRedondo commented 1 year ago

The examples in the official documentation to make API calls from a React app (1, 2 and 3) are insufficient. The abstraction used is not fully fit for production. The fact that you can only pass the access token to the API function from within a React component is restrictive and repetitive for most use cases in a real world application.

The official examples should be further expanded to include better abstractions, like the one on the above https://github.com/auth0/auth0-react/issues/67#issuecomment-661874041 or https://github.com/auth0/auth0-react/issues/67#issuecomment-662566676, or when widely used libraries like Axios are used (How to get Auth0 token from outside React components? - Stack Overflow).

These better abstractions should be covered officially. Many people are asking for them:

raphox commented 8 months ago

In the previous comments, I need to get the token in all components that I need to make a new request. I choose another way to get the token using cacheLocation="localstorage":

// My parent component:

<Auth0Provider
  domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
  clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
  cacheLocation="localstorage"
  useRefreshTokens={true}
  authorizationParams={{
    scope: "profile",
    redirect_uri: typeof window !== "undefined" && window.location.origin,
  }}
>
...
</Auth0Provider>

// Axios settings:

import axios from "axios";
import { LocalStorageCache, CacheKey } from "@auth0/auth0-spa-js";

const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_ACCOUNTING_API_URL,
  timeout: 5000,
  headers: {
    Accept: "application/json",
  },
});

instance.interceptors.request.use(function (config) {
  const cache = new LocalStorageCache();
  const cacheKey = new CacheKey({
    clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID,
    scope: "@@user@@",
  });

  const data = cache.get(cacheKey.toKey());
  const token = data?.decodedToken?.claims["custom-access-token"];

  config.headers.Authorization = `Bearer ${token}`;
  config.params = { ...config.params, token };

  return config;
});
...

export default instance;

// My hook to be used in my components:

import { keepPreviousData, useQuery } from "@tanstack/react-query";
import api from "./index"; // <-- importing Axios
import { dataToPagination } from "@/lib/utils";

export function useStatements({ pageIndex, pageSize }) {
  const query = {
    offset: pageIndex * pageSize,
    limit: pageSize,
  };

  const { isPending, error, data } = useQuery({
    queryKey: ["statements", query.offset],
    queryFn: () => findAll(query),
    placeholderData: keepPreviousData,
  });

  return {
    isPending,
    error,
    ...dataToPagination(data, { key: "statements", pageSize }),
  };
}

export const findAll = async (query) => {
  const response = await api.get("statements.json", {
    params: { ...query },
  });

  return response.data;
};