With a little help from Axios
This middleware helps with:
- Dispatching Flux Standard Actions for API Calls
- Adding a JWT authorization header for authenticated calls
- Proactively refreshing expired (or near-expired) tokens
It ships with:
apiMiddleware
- The middlewareAuth
- An Auth class to help saving stuff to localStoragecreateDataReducer
- A reducer creator to help create simple reducers to save and delete auth datacheckAuth
- An High Order Component to protect routes
A simple action requires:
types
: Array with one element for each of the action lifecycle (start, complete, error)callAPI
: Function to make the request
export const fetchSomething = () => ({
callAPI: () => fetch("http://localhost:3000/something")
types: ["START", "COMPLETE", "ERROR"]
]
})
Auth's constructor accepts an object with 3 optional parameters:
// Somewhere that can easily be imported (e.g.: index.js)
const auth = new Auth({
// localStorage key to save extra auth data
dataKey: "auth_data"
// localStorage key to save access token
accessToken: "authToken_access"
// localStorage key to save refresh token
refreshToken: "authToken_refresh"
})
login
: Accepts an object withdata
,accessToken
andrefreshToken
. Will save them to localStorage.logout
: Removes all three from localStorageisAuthed
: Returns a boolean to check whether there's an access token or not in localStoragesaveToken
: Saves the access tokensaveRefreshToken
: Saves the refresh tokensaveData
: Saves auth extra datagetToken
: To retrieve the access tokengetRefreshToken
: To retrieve the refresh tokengetData
: To retrieve the extra auth data
apiMiddleware({
/*
An Auth Class instance that the middleware will use to manage the tokens in localStorage.
It's recommended that you initialize your Auth somewhere else (e.g. index.js) and then import it here
*/
auth: new Auth(),
/*
The API's base URL. It will be fed to an Axio's instance
*/
baseUrl: "http://yourapi.com/api",
/*
Function to parse the token to the Authorization header
*/
parseToken: token => `Bearer ${token}`,
/*
Function to make the refresh request.
This function receives an axios instance (with the base URL already set) and the token
*/
makeRefreshTokenCall: (axios, token) =>
axios.post("/refresh", { refreshToken: token }),
/*
Function to get the tokens from the server repsonse.
It receives the server response and must return an object with two keys:
- accessToken
- refreshToken
*/
getTokenFromResponse: res => ({
accessToken: res.jwt,
refreshToken: res.jwt_refresh
})
});
Example
// in configureStore.prod.js
import { createStore, applyMiddleware } from "redux";
import reducers from "../reducers";
// Auth's instance
import { auth } from "../";
const configureStore = preloadedState => {
const middleware = [
apiMiddleware({
auth,
baseUrl: "https://yourproductionapi.com/api",
parseToken: token => `Bearer ${token}`,
makeRefreshTokenCall: (axios, token) =>
axios.post("/auth/user/refresh", { jwt_refresh: token }),
getTokenFromResponse: res => ({
accessToken: res.jwt,
refreshToken: res.jwt_refresh
})
})
];
const store = createStore(
reducers,
preloadedState,
applyMiddleware(...middleware)
);
return store;
};
export default configureStore;
Actions accept the following keys:
- types
- callAPI
- shouldCallAPI
- meta
It is mandatory for types to be an Array or either Strings or Objects
It is mandatory for types
to be an Array with one element for each of the action lifecycle:
'START', 'COMPLETE', 'ERROR' (In this specific order).
- Each must be a String or an Object (with 'type' and 'payload').
- The Object's payload key must be a function that will be given the server response, dispatch function and state object.
{
types: ["START", "COMPLETE", "ERROR"];
}
{
types: [
"START",
{
type: "COMPLETE",
payload: res => res.data
},
"ERROR"
];
}
{
types: [
"START",
{
type: "COMPLETE",
payload: res => normalize(res.data, schema.data)
},
{
type: "ERROR",
payload: res => res.error.id
}
];
}
callAPI
, as the name says, is the function to call the API. It will be given two arguments:
- an axios instance: with the base url and a header interceptor to add authorization headers
- the redux state
{
callAPI: () => fetch("https://someurl.com/");
}
{
callAPI: axios => axios.get("https://someurl.com/");
}
{
callAPI: (axios, state) => {
return axios.post(`/users/${state.user.id}/profile`, {
email: "[email protected]"
});
};
}
A Function to evaluate if the request should return early. It is given the state and should return a boolean.
shouldCallAPI: () => true;
shouldCallAPI: state => !state.data.isFetching;
The meta
object will be forwarded in each of the actions
meta: {
someData: "This will be available in every action dispatched";
}
export const getUserData = id => ({
shouldCallAPI: state => !state.profile.isFetching,
callAPI: api.get(`/users/${id}`),
types: [
"FETCH_PROFILE_START",
{
type: "FETCH_PROFILE_COMPLETE",
payload: response => response.data
},
"FETCH_PROFILE_ERROR"
],
meta: {
userId: id
}
});
// in reducers/index.js
export default combineReducers({
authData: createDataReducer({
// Array of types that will make the reducer save the payload
addDataTypes: [types.LOGIN_COMPLETE],
// Array of types to revert to the initialState
removeDataTypes: [types.LOGOUT],
// Initial State (Defaults to {})
initialState: {}
})
// Your other reducers here
});
checkAuth HOC (with React Router)
An usual implementation will load the reducer above as the store's preloaded state:
<Provider store={configureStore({ authData: auth.getData() || {} })}>
This High Order Component will make use of that piece of state to protect routes.
checkAuth
is a high-order-function that accepts an Object with the following keys:
checkAuth({
/*
Function to map the state to a boolean that evaluates the authed status
*/
mapIsAuthedToProps: state => Object.keys(state.authData).length > 0
/*
Boolean to define whether the HOC should redirect when the user is authed or not. Can be useful if some pages are available only for guest users.
Defaults to true
*/
requireAuthentication: true
/*
Route to redirect to (using React Router's Redirect component).
Defaults to "/login"
*/
redirectTo: "/login"
})
An usual implementation of this High Order Component is to create a file src/hoc/checkAuthentication.js
and create some defaults there:
const mapIsAuthedToProps = state => Object.keys(state.authData).length > 0;
export const requireAuth = checkAuth({
mapIsAuthedToProps
});
export const requireGuest = checkAuth({
mapIsAuthedToProps,
requireAuthentication: false,
redirectTo: "/profile"
});
Then use it where you define the Routes:
<Route exact path="/profile" component={requireAuth(Profile)} />
<Route exact path="/login" component={requireGuest(Login)} />
Check source code for an example app.