Extension to your API middleware to refresh access tokens when a request hits a 401 response (access token expired).
If you use transient (short-lived) OAuth tokens for authentication then these will expire all the time. However, instead of logging the user out every time the token expires it is much better to simply request a new token using the refresh token.
If you use Redux you are surely using an API middleware for requesting an API. You might roll your own, use the one from the real-world example or a 3rd party like agraboso's redux-api-middleware. I use the latter - you should give it a try. It is great.
This library provides a function you can plug into your API call. If the API call returns a 401 response then your access token probably expired. This library will try to refresh the token. Whilst refreshing it will queue up all API calls. If the refresh was successful it will retry all the queued calls. If it was not successful it will log out the user.
I have made a small example app here
Your API middleware must return FSA-style responses. There is a great description about flux-standard-actions here. You want the quick run down? Your middleware must return actions like this.
Successful response (status range 200-299)
{
type: "ACTION",
payload: {
// API response
}
}
Error response (everything else)
{
type: "ACTION",
error: true,
payload: {
status: 401 // Http status code
}
}
This is the most important thing. redux-refresh-token will look for the error
property and will look for the status
to determine whether
or not it should try a refresh. It will always attempt a refresh on 401
status codes. Everything else will just be ignored.
First install with npm
npm install --save redux-refresh-token
You will need to install a reducer under the key tokenRefresh
. If you want to change the key
you have to pass the refreshReducerKey
setting to attemptRefresh
.
import { reducer } from 'redux-refresh-token'
const store = createStore(
combineReducers({
tokenRefresh: refreshReducer,
// Other reducers
}),
{},
applyMiddleware(api)
)
So you probably have some API middlware in your code that ends a long the lines of this below
return fetch(`${API_ROOT}${endpoint}${querystring}`, {
method,
body,
headers,
credentials: "same-origin"
})
.then(response => response.json().then(json => {
if (!response.ok) {
return Promise.reject(json)
}
return json
}))
.then(response => next({
type: successType,
payload: response
})),
error => next({
type: failureType,
error: true,
payload: {
status: response.status,
error: error.message
}
}))
Now replace it with
import attemptRefresh, { createFSAConverter } from "redux-refresh-token";
// Middleware code skipped for breweity
return fetch(`${API_ROOT}${endpoint}${querystring}`, {
method,
body,
headers,
credentials: "same-origin"
})
// This step will convert fetch's response into a FSA style action
// This is needed in the attempt method. If you use redux-api-middleware
// the middleware already returns FSA style actions, so you do not need
// this step.
.then(createFSAConverter(successType, failureType))
.then(
attemptRefresh({
// An action creator that determines what happens when the refresh failed
failure: logout,
// An action creator that creates an API request to refresh the token
refreshActionCreator: attemptTokenRefresh,
// This is the current access token. If the user is not authenticated yet
// this can be null or undefined
token,
// The next 3 parameters are simply the arguments of
// the middleware function
action,
next,
store,
})
)
The attemptRefresh
has various parameters to adjust to different types of API middleware.
Should be an actionCreator. This action will be dispatched whenever the refresh failed. Usually this would be a logout action.
An example could be
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'
export const logout = () => ({
[CALL_API]: {
endpoint: '/logout',
method: 'POST',
types: [
LOGOUT_REQUEST,
LOGOUT_SUCCESS,
LOGOUT_FAILURE
]
}
})
When refreshing it is important to determine whether or not a failed action is an attempt to refresh the access token. Because the library will attempt to refresh all 401's, if the refresh call returns 401 and we do not check if successfully we are stuck in an infinite loop.
The default behaviour is to check like below
return action["Call API"].endpoint === refreshAction["Call API"].endpoint;
An action creator that creates the action that will request the API for a new token.
Example
export const TOKEN_REFRESH_REQUEST = 'TOKEN_REFRESH_REQUEST'
export const TOKEN_REFRESH_SUCCESS = 'TOKEN_REFRESH_SUCCESS'
export const TOKEN_REFRESH_FAILURE = 'TOKEN_REFRESH_FAILURE'
export const attemptTokenRefresh = () => ({
[CALL_API]: {
endpoint: '/login/refresh',
method: 'POST',
types: [
TOKEN_REFRESH_REQUEST,
TOKEN_REFRESH_SUCCESS,
TOKEN_REFRESH_FAILURE
]
}
})
By default the reducer should be installed under the key tokenRefresh
but you can
change the default setting using this key.
An action creator used to set a new access token in the redux store state.
The default creator is given below
export const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN'
export const setAccessToken = ({ access_token, expires_in }) => ({
type: SET_ACCESS_TOKEN,
access_token,
expires_in
})
The current access token from the redux store state.
npm test
The MIT License (MIT). Please see License File for more information.