Have a bit of an issue attempting to get Auth0 info on the logged-in user with our current architecture.
We have redux
with @reduxjs/toolkit
& react-redux
as our state management tool.
We use axios
to make HTTP requests via redux-thunk actions.
And now we have a part of our application that allows users to signup/login with Auth0
.
So, an example of our problem.
Currently our redux store is setup with some reducers
/* eslint-disable import/no-cycle */
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
const createStore = (initialState?: any) => {
return configureStore({
reducer: {
// reducers are here
},
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
Then we attached that to a Provider
at the base of our application
import React from 'react';
import { Provider } from 'react-redux';
import createStore from '../store/createStore';
const App = () => {
return (
<Provider store={createStore()}>
//
</Provider>
);
};
export default App;
We have an axios instance function that uses axios
to make HTTP requests and handles errors.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
// I need to access auth0 data here
const { getAccessTokenSilently, isAuthenticated, isLoading, loginWithRedirect, user } = auth0;
if (!token) {
const tokenErr = {
title: 'Error',
message: 'Missing Authentication Token',
success: false,
};
throw tokenErr;
}
try {
let accessToken = token;
// Update authorization token if auth0 user
if(auth0) {
if(isAuthenticcation && user) accessToken = await getAccessTokenSilently({ audience });
else loginWithRedirect();
}
const result = await axios({
...requestConfig,
headers: {
...requestConfig.headers,
authorization: `Bearer ${accessToken}`,
},
});
return result;
} catch (error: any) {
if (error.response) {
if ([401, 403].includes(error.response.status)) {
window.location = '/';
}
const contentType = error?.response?.headers?.['content-type'];
const isHTMLRes = contentType && contentType.indexOf('text/html') !== -1;
const errObj = {
status: error?.response?.status,
statusText: error?.response?.statusText,
errorMessage: isHTMLRes && error?.response?.text && (await error?.response?.text()),
error,
};
throw errObj;
}
throw error;
}
};
export default axiosInstance;
This in an example of a thunk action, we would have something like this that uses the axios instance mentioned above to make the HTTP requests.
import axios, { API_URL } from '../../services/axios';
import { Result } from '../../types/test';
import { AppThunk } from '../../store/store';
import { setResults, setResultsLoading, setTableLoading } from './test.slice';
type DefaultThunk = () => AppThunk<Promise<void>>;
const getResults: DefaultThunk = () => async () => {
dispatch(setTableLoading(true));
try {
const result = await axios<Result[]>(
{
method: 'GET',
url: `${API_URL}/test`,
},
);
dispatch(setResults(result.data));
} catch (err: any) {
console.log({ err });
} finally {
dispatch(setResultsLoading(false));
dispatch(setTableLoading(false));
}
};
export default getResults;
We then dispatch our thunk actions to make HTTP requests and update reducer states in our React components.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import getResults from '../../reducers/test/test.thunk';
const TestComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getResults());
}, []);
return (
//
);
};
export default TestComponent;
My problem is that I have no idea how to integrate Auth0
gracefully into the current flow, so I do not have to make checks in every react component that uses a thunk action.
Basically I need access to values within the useAuth0
hook from @auth0/auth0-react
for example getAccessTokenSilently
, isAuthenticated
, user
& loginWithRedirect
. Just to name a few.
We can't use the useAuth0
hook in the axios instance file, as it's not a react component/hook, nor is the thunk file.
So I'm not sure how and where the best place is to get the data so that it is accessible in the axios file, as aforementioned without having to pass it as an argument or something in every redux thunk action.
Perhaps we just need a different approach to the current flow of dispatch > action > axios request?
Is there any way to pass this data in as middleware to redux?
Any help would be greatly appreciated.
I don't believe you'd be able to use a middleware to "sniff" out the auth0
context value because middlewares run outside React. What I'd suggest here is to create a wrapper component that sits between the Auth0Provider
and redux Provider
components that accesses the auth0
context and dispatches an action to save it into the redux state where it can be selected via useSelector
or accessed directly from store.getState()
.
Fortunately it appears the auth0
context value is already memoized here so it should be able to be directly consumed as a stable reference within the app.
Rough Example:
import { useDispatch } from 'react-redux';
import { useAuth0 } from '@auth0/auth0-react';
import { actions } from '../path/to/auth0Slice';
const Auth0Wrapper = ({ children }) => {
const dispatch = useDispatch();
const auth0 = useAuth0();
useEffect(() => {
dispatch(actions.setAuthContext(auth0));
}, [auth0]);
return children;
};
Create and export the store for consumption within the app.
Store
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import { combineReducers } from 'redux';
...
import auth0Reducer from '../path/to/auth0Slice';
...
const rootReducer = combineReducers({
auth0: auth0Reducer,
... other root state reducers ...
});
const createStore = (initialState?: any) => {
return configureStore({
reducer: rootReducer,
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
App
import Auth0Wrapper from '../path/to/Auth0Wrapper';
import createStore from '../path/to/store';
const store = createStore();
const App = () => {
return (
<Auth0Provider ......>
<Provider store={store}>
<Auth0Wrapper>
// ... JSX ...
</Auth0Wrapper>
</Provider>
</Auth0Provider>
);
};
export store;
export default App;
Create a new Auth0 state slice.
import { createSlice } from '@reduxjs/toolkit';
const auth0Slice = createSlice({
name: 'auth0',
initialState: {},
reducers: {
setAuthContext: (state, action) => {
return action.payload;
},
},
});
export const actions = {
...auth0Slice.actions,
};
export default auth0Slice.reducer;
From here you can import the exported store
object and access the current state inside the axios setup.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import store from '../path/to/App';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
const { auth0 } = store.getState(); // <-- access current state from store
const {
getAccessTokenSilently,
isAuthenticated,
isLoading,
loginWithRedirect,
user
} = auth0;
...
};