How to implement the refresh token in react-redux - reactjs

The following code I did allows to refresh the access token, when on the first request the response return an Unauthorized, run the service to refresh the access token.
The code works fine, the refresh token service runs on the backend made in Java.
The code is incomplete, there is only the call to the refresh token method.
On the following shown the code.
const apiMiddleware = ({ dispatch }) => (next) => (action) => {
const result = next(action);
if (action.type !== API) {
return result;
}
const {
url,
method,
data,
onSuccess,
types,
} = action.payload;
const axiosInstance = createAxios();
const refreshToken = async(originalRequest) => {
const axiosInstance = createAxios();
const data = authUtil.getUserLoggedIn();
const response = await axiosInstance({
url: `${AUTH_ENDPOINT}/refresh-token`,
method: 'PUT',
data: { token: data.refreshToken },
});
if (response.status === 200) {
authUtil.setUserLoggedIn(response.data);
originalRequest.headers['Authorization'] = response.data.accessToken;
axiosInstance(originalRequest)
.then(({ data }) => {
handlerSuccess({ data });
return result;
});
} else {
dispatch(apiError(types[1], response.error));
}
};
const handlerSuccess = ({ data }) => {
dispatch(onSuccess(data));
};
axiosInstance({
method,
url,
data,
})
.then(({ data }) => {
handlerSuccess({ data });
return result;
})
.catch((error) => {
if (error.response && error.response.status === 403) {
dispatch(accessDenied(types[2], window.location.pathname));
} else if (error.response && error.response.status === 401) {
refreshToken(error.config);
}
return result;
})
};
export default apiMiddleware;
Somebody can help me to improve this code

Related

React-Admin Simple Refresh JWT Token

I have an react-admin with an auth provider like the below.
I want to refresh my token, but I don't know how to do it.
I tried to follow this blog post, but my auth is a little different and I can't make it work (the error "httpClient(...).then" is not a function and others make me leave it).
I can make it with a more simple solution, does not need to be in memory. I tried to call my refresh endpoint to get my refresh token, but my call go without the current token.
My endpoint to refresh the token is:
/auth/jwt/refresh
I need to call it like this:
curl -X 'GET' \
'http://localhost:8000/auth/jwt/refresh' \
-H 'accept: application/json' \
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
And My response body would be: (and I need to save it to my localstorage or the in memory way)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZTUwZDdhZDctOWE5Ni00NzQyLTgxNWEtZTNmZmJmNGRiMTVjIiwiYXVkIjpbImZhc3RhcGktdXNlcnM6YXV0aCJdLCJleHAiOjE2Mzk4NDE1MDF9.-o2yk56sCj_MZx_VA6PxH7gZ-KKSMmopbDNDiapHmn0",
"token_type": "bearer"
}
My inMemoryJWTManager file:
const inMemoryJWTManager = () => {
let inMemoryJWT = null;
let isRefreshing = null;
let logoutEventName = 'ra-logout';
let refreshEndpoint = '/auth/jwt/refresh';
let refreshTimeOutId;
const setLogoutEventName = name => logoutEventName = name;
const setRefreshTokenEndpoint = endpoint => refreshEndpoint = endpoint;
// This countdown feature is used to renew the JWT before it's no longer valid
// in a way that is transparent to the user.
const refreshToken = (delay) => {
refreshTimeOutId = window.setTimeout(
getRefreshedToken,
delay * 1000 - 5000
); // Validity period of the token in seconds, minus 5 seconds
};
const abordRefreshToken = () => {
if (refreshTimeOutId) {
window.clearTimeout(refreshTimeOutId);
}
};
const waitForTokenRefresh = () => {
if (!isRefreshing) {
return Promise.resolve();
}
return isRefreshing.then(() => {
isRefreshing = null;
return true;
});
}
// The method make a call to the refresh-token endpoint
// If there is a valid cookie, the endpoint will set a fresh jwt in memory.
const getRefreshedToken = () => {
const request = new Request(refreshEndpoint, {
method: 'GET',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'include',
});
isRefreshing = fetch(request)
.then((response) => {
if (response.status !== 200) {
ereaseToken();
global.console.log(
'Token renewal failure'
);
return { token: null };
}
return response.json();
})
.then(({ token, tokenExpiry }) => {
if (token) {
setToken(token, tokenExpiry);
return true;
}
ereaseToken();
return false;
});
return isRefreshing;
};
const getToken = () => inMemoryJWT;
const setToken = (token, delay) => {
inMemoryJWT = token;
refreshToken(delay);
return true;
};
const ereaseToken = () => {
inMemoryJWT = null;
abordRefreshToken();
window.localStorage.setItem(logoutEventName, Date.now());
return true;
}
// This listener will allow to disconnect a session of ra started in another tab
window.addEventListener('storage', (event) => {
if (event.key === logoutEventName) {
inMemoryJWT = null;
}
});
return {
ereaseToken,
getRefreshedToken,
getToken,
setLogoutEventName,
setRefreshTokenEndpoint,
setToken,
waitForTokenRefresh,
}
};
export default inMemoryJWTManager();
This is my auth provider: (updated, using inMemoryJWTManager)
import inMemoryJWTManager from './inMemoryJWT'
const apiUrl = 'http://localhost:8000'
const authProvider = {
login: ({username, password}) => {
const oAuthParams = {
username,
password
}
const body = Object.keys(oAuthParams).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(oAuthParams[key]);
}).join('&');
const request = new Request(`${apiUrl}/auth/jwt/login`, {
method: 'POST',
body: body,
headers: new Headers({'Content-Type': 'application/x-www-form-urlencoded'}),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(( {access_token} ) => {
inMemoryJWTManager.setToken(access_token);
});
},
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
inMemoryJWTManager.ereaseToken();
return Promise.reject({redirectTo: '/login'});
}
// other error code (404, 500, etc): no need to log out
return Promise.resolve();
},
checkAuth: () => inMemoryJWTManager.getToken()
? Promise.resolve()
: Promise.reject({ message: 'Login necessário', redirectTo: 'login' }),
logout: () => {
inMemoryJWTManager.ereaseToken();
return Promise.resolve();
},
getPermissions: () => {
return inMemoryJWTManager.getToken() ? Promise.resolve() : Promise.reject();
},
};
export default authProvider;
My updated httpClient code using inMemoryJWTManager: (and I'm using: const dataProvider = jsonServerProvider(apiUrl, httpClient); with modifications to it, but I think it is irrelevant)
const httpClient = (url) => {
const options = {
headers: new Headers({ Accept: 'application/json' }),
};
const token = inMemoryJWTManager.getToken();
console.log(token)
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
} else {
inMemoryJWTManager.setRefreshTokenEndpoint(`${apiUrl}/auth/jwt/refresh`);
return inMemoryJWTManager.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set('Authorization', `Bearer ${inMemoryJWTManager.getToken()}`);
};
return fetchUtils.fetchJson(url, options);
});
}
};
My problem is that, when I call my refresh token endpoint, my request go without the {'Authorization': Bearer... and it is not renewed and I got logged out. The other endpoints are fine, they go with the token.
You must check token expire before each requests, if token expired you must get new from /auth/jwt/refresh, then you can send current request. All this information is in the article post. Example:
const httpClient = (url) => {
const options = {
headers: new Headers({ Accept: 'application/json' }),
};
const token = inMemoryJWT.getToken();
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
} else {
inMemoryJWT.setRefreshTokenEndpoint('http://localhost:8001/refresh-token');
return inMemoryJWT.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set('Authorization', `Bearer ${inMemoryJWT.getToken()}`);
};
return fetchUtils.fetchJson(url, options);
});
}
};

React Redux handling response after axios resubmit original request

I have a small booking program to practice ReactJS + Redux + ReduxSaga + Axios + Axios Interceptor + JWT authentication
Here is the code in the component BookingDialog after the submit button click
bookingDialog.js
const handleSubmit = (event) => {
event.preventDefault();
let payload = {
selectedDate: selectedDate,
carId : carDetail.id,
userId : user.login.id, //or pass by jwt accesstoken
remarks: remarks
}
console.log(payload);
dispatch(createBooking(payload));
}
And there is saga watcher which take latest of action createBooking to function handleCreateBooking
bookingSaga.js
export function* handleCreateBooking(action) {
try {
const response = yield call(createBooking, action.payload);
const { data } = response;
console.log("handleCreateBooking");
console.log(response);
if (data && data.result && data.result > 0){
console.log("booked successfully");
yield put(setMessageBarOpen({type: "success", content: "booked successfully"}));
yield put(setCreateBookingOpen(false));
}
else{
console.log("booked failed");
//yield put(setMessageBarOpen({type: "error", content: "booked failed"}));
//yield put(setCreateBookingOpen(false));
}
} catch (error) {
console.log(error);
}
}
bookingRequest.js
const createBooking = (payload) => {
return postUrl(apiURL.createBooking.url, payload).then((res) => {
return res
});
}
The program works as expected. Success message shown and booking dialog closed after submission.
If the jwt is expired, the program will retrieve the access token by refresh token and resubmit the original request with the new access token.
The problem is that, after the original request is sent and booking is created successfully, the follow up actions (setMessageBarOpen & setCreateBookingOpen) are not performed as the posting of original request is not under the function handleCreateBooking in bookingSaga.js
axiosInstance.js
import axios from 'axios';
import apiURL from "requests/apiURL";
const ax = axios.create();
ax.interceptors.request.use(
request => {
const accessToken = JSON.parse(localStorage.getItem('token')) && JSON.parse(localStorage.getItem('token')).accessToken;
if (accessToken) {
let auth = false;
for (const [key, value] of Object.entries(apiURL)) {
if (request.url.includes(value.url)) {
auth = value.auth;
break;
}
}
if (auth) {
request.headers.authorization = `Bearer ${accessToken}`;
}
}
return request;
},
error => {
return Promise.reject(error);
}
);
const sendRefreshToken = (refreshToken) => {
return new Promise((resolve, reject) => {
console.log("refreshToken");
postUrl(apiURL.token.url, { token: refreshToken })
.then((res) => {
console.log(res);
if (res.data) {
console.log(res.data);
localStorage.setItem('token', JSON.stringify({accessToken: res.data.accessToken, refreshToken: refreshToken}));
resolve(res);
}
})
.catch(error => {
reject(error);
});
})
}
ax.interceptors.response.use(
(response) => {
return response;
},
error => {
console.log("axios.interceptors.response");
console.log(error);
const status = error.response ? error.response.status : null;
const originalRequest = error.config;
let isRefreshing = false;
if (status === 403) {
if (!isRefreshing) {
const refreshToken = JSON.parse(localStorage.getItem('token')) && JSON.parse(localStorage.getItem('token')).refreshToken;
console.log("403, refreshToken:");
console.log(refreshToken);
isRefreshing = true;
sendRefreshToken(refreshToken)
.then(({ status }) => {
console.log(status);
if (status === 200 || status === 204) {
isRefreshing = false;
console.log("start resendRequest");
console.log(originalRequest);
return ax(originalRequest);
}
})
.catch(error => {
console.error(error);
});
}
}
return error;
}
);
export const getUrl = async (url, opt) => {
const response = await ax.get(url, opt);
return response;
}
export const postUrl = async (url, data, opt) => {
const axios_res = await ax.post(url, data, opt);
return axios_res;
}
How should I handle the response from the resubmitted original request?
Thanks.

Axios PUT Data with Params

My backend API route is /api/updateUser/:id
How am I supposed to POST data into this API? I'm familiar with POST request for non params APIs but this one has an /:id in the route.
Can someone show me an example with this demo code
state = {
username: "random123",
password: "random123",
userid: "qwertyuiop",
};
saveDetails = async () => {
const { username, password, userid } = this.state;
let data = new FormData();
data.append('username',username);
data.append('password',password);
axios
.put(apiEndPoint+'?id='+this.state.userid, data) //this is where I need help
.then(async (response) => {
if (response.data) {
console.log("success");
} else {
console.log("issue");
}
})
.catch((err) => {
console.log("error",err);
});
};
This is the working example for Path Parameter Axios PUT request -
saveDetails = async () => {
const { username, password, userid } = this.state;
axios
.put(apiEndPoint+"updateUser/"+userid, {
username:username,
password:password,
})
.then(async (response) => {
if (response.data) {
console.log("done");
} else {
console.log("error");
}
})
.catch((err) => {
console.log("error",err);
});
};

Resending a request that was made with an expired token is leading to status pending in developer tools

I have a react application where I am trying to implement JWT.
I am using the axios interceptor where I catch status 401 returned by the server due to expired token, send the refresh token to server, receive the new access token in the client and then resend the original failed request.
The problem I am facing is that, when I resend the original failed request, the status appears as pending forever in the developer tools, network tab. The original failed request is a POST request, when I checked the database it was updated. So why is it showing pending status in the developer tools ?
Here is my axios interceptor code
import axios from 'axios'
// import refreshToken from '../src/Store/refreshToken'
import { store } from '../src/index'
import { removeAuth } from '../src/Store/actions/authAction'
const api = axios.create({
baseURL: process.env.REACT_APP_SERVER
})
function createAxiosResponseInterceptor(axiosInstance) {
axiosInstance.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
if (token){
config.headers.Authorization = token;
}
return config
}
)
axiosInstance.interceptors.response.use(
response => {
return response;
},
error => {
var errorStatus = error.response.status;
if (errorStatus === 401){ // status 401 is used when token is expired
let cookies = document.cookie
let refresh = cookies.split("refresh=")[1].split(';')[0]
if(!sendRefreshToken(refresh, error)) {
store.dispatch(removeAuth({isLoggedIn: false}));
localStorage.setItem('token', '');
document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
}
return error
}
);
}
function sendRefreshToken(refreshToken, error) {
let result = api.post('/refresh', {
refreshToken: refreshToken
})
.then(response => {
if (response.data.success && response.data.message === "new access token set") {
localStorage.setItem('token', response.data.newToken)
api({ // Here I am resending the failed request.
method: error.response.config.method,
url: error.response.config.url,
data: JSON.parse(error.response.config.data)
}).then(response => {
console.log(response)
return true
})
.catch(error => {
console.log(error)
return false
})
}
})
.catch(error => {
console.log(error)
return false
})
return result
}
createAxiosResponseInterceptor(api);
export default api;
Please let me know if you find anything wrong with the code. Let me know if this is the right way to do it. Open to offer more bounty points.
Consider this article for reference.
https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
import axios from 'axios'
// import refreshToken from '../src/Store/refreshToken'
import { store } from '../src/index'
import { removeAuth } from '../src/Store/actions/authAction'
const api = axios.create({
baseURL: process.env.REACT_APP_SERVER
})
function createAxiosResponseInterceptor(axiosInstance) {
axiosInstance.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
if (token){
config.headers.Authorization = token;
}
return config
}
)
axiosInstance.interceptors.response.use(
response => {
return response;
},
error => {
var errorStatus = error.response.status;
const originalRequest = error.config;
if (
error.response.status === 401 &&
!originalRequest._retry
) {
originalRequest._retry = true;
return api
.post('/refresh', {
refreshToken: getRefreshToken()
})
.then((jsonRefreshResponse) => {
if (jsonRefreshResponse.status === 200) {
// 1) put token to LocalStorage
saveRefreshToken(
jsonRefreshResponse.data.refreshToken
);
// 2) Change Authorization header
const newAccessToken = getJwtToken();
setAuthHeader(newAccessToken);
// 3) return originalRequest object with Axios.
// error.response.config.headers[
// "Authorization"
// ] = `Bearer ${newAccessToken}`;
setAuthHeader(newAccessToken)
return axios(error.response.config);
}
})
.catch((err) => {
console.warn(err);
})
}
if (error.config) {
console.log(error.config);
return Promise.reject();
}
}
);
}
export const setAuthHeader = (token) => {
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
};
createAxiosResponseInterceptor(api);
export default api;
//These methods could be in separate service class
const getJwtToken=()=> {
return localStorage.getItem("token");
}
const getRefreshToken=() =>{
return localStorage.getItem("refreshToken");
}
const saveJwtToken=(token)=> {
localStorage.removeItem("token");
localStorage.setItem("token", token);
}
const saveRefreshToken=(refreshToken)=> {
localStorage.setItem("refreshToken", refreshToken);
}

How to call ToastsStore.success or ToastsStore.error after API response in React component?

I created a component that contains the "New Article" form. The user can add a new article after clicking the Save button. The click event calls this.props.fetchAddPaper(data), which saves the article to the database.
If the response is 200, I would like to display information on the page for the user that the article has been successfully saved.
If the response is 500 or 400 or 401, I would like to display information that 'something went wrong try again'. To display alerts I use react-toasts. My question is: how can I get a response from the API after clicking the Save button so that you can display a success or error alert? How do I get a response from this.props.fetchAddPaper (data) in the handleSubmit method that I am calling?
Below is the fetchAddPaper that connects to the API. How do I get a response from such a method in a component?
const apiMiddleware = ({ dispatch }) => next => action => {
next(action);
if (action.type !== 'API')
return;
let {
url, // Endpoint address, relative to $HOST/api/
method, // http method (GET, POST, DELETE etc.)
params, // URI string params
data, // Post data
onSuccess, // Function accepting response. If redux action is returned, it will be dispatched
onFinish, // Function run on either success or error
onError, // Function accepting error
onValidationError, // Function accepting response with validation error
text, // Loading text. If not provided there will be no overlay while loading data
successText // Success text, shown on green bar. If not provided it won't be shown
} = action.payload;
// Allow for onSuccess, onFinish and onError to be either redux (and thunk) actions or normal functions
const conditionalDispatch = (action) =>
action && _.isFunction(action) ? dispatch(action) : action;
const request = {
headers: {
'Accept': 'application/json'
},
url: `${host}/api/${url}`,
method,
timeout: 180000
};
if (params) {
params = { ...params };
for (let prop in params) {
if (Array.isArray(params[prop])) {
const arrayData = arrayToGetParameters(params[prop], prop);
delete params[prop];
Object.assign(params, arrayData);
}
}
}
if (data) {
if (method.toUpperCase() === "GET" || method.toUpperCase() === "DELETE") {
throw new Error("Can't add request data to get or delete method");
}
request.headers['Content-Type'] = 'application/json;text/plain;text/json';
}
request.data = data;
request.params = params;
text && dispatch(onLoadingStart(text));
let notificationId = shortId.generate();
axios.request(request)
.then((response) => {
text && dispatch(onLoadingEnd());
onSuccess && dispatch(onSuccess(response.data));
onFinish && conditionalDispatch(onFinish);
if (successText) {
dispatch(onAddFlashMessage({type: 'success', text: successText, id: notificationId}));
setTimeout(() => {
dispatch(onDeleteFlashMessage(notificationId));
}, 5000);
}
})
.catch((error) => {
onFinish && conditionalDispatch(onFinish);
// onError && conditionalDispatch(onError(error));
onError && dispatch(onError(error));
dispatch(onLoadingEnd());
if (error.response && error.response.status === 401) {
//dispatch(onLogOut()); todo: wylogowanie
return;
}
if (error.response && error.response.status === 422 && onValidationError) {
conditionalDispatch(onValidationError(error));
}
else {
dispatch(onAddFlashMessage({...httpReqErrorHandler(error), id: notificationId}));
}
setTimeout(() => {
dispatch(onDeleteFlashMessage(notificationId));
}, 5000);
});
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ON_FETCH_ADD_PAPER:
return {
...state,
paper: action.response
};
default:
return state;
}
const onFetchAddPaper = (response) => ({ type: actionTypes.ON_FETCH_ADD_PAPER, response });
export const fetchAddPaper = (data) => {
return (dispatch) => {
dispatch({
type: 'API',
payload: {
url: 'Papers/addPaper',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
},
data: data,
onSuccess: (response) => onFetchAddPaper(response),
onError: (error) => onFetchAddPaper(error)
}
});
};
};
handleSubmit(e) {
e.preventDefault();
let data = {
title: this.state.title,
header: this.state.header
}
this.props.fetchAddPaper(data);
console.log(this.props.paper);
//when the user first clicks the save button, the response is empty, but the second time the response has a value 200
}
function mapStateToProps(state) {
return {
paper: state.paper.paper
}
};
function mapDispatchToProps(dispatch) {
return {
fetchAddPaper: data => dispatch(fetchAddPaper(data))
}
}
//initialstore.jsx
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import apiMiddleware from './ApiMiddleware';
import rootReducers from '../RootReducers';
export default function initStore() {
const store = createStore(
rootReducers,
compose(
applyMiddleware(thunk, consoleMessages, apiMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f
)
);
if (module.hot) {
module.hot.accept('../RootReducers', () => {
const nextRootReducer = require('../RootReducers').default;
store.replaceReducer(nextRootReducer);
});
}
return store;
}
You can return a promise from your fetchAddPaper action
Something like this:
export const fetchAddPaper = (data) => {
return (dispatch) => {
return new Promise((resolve,reject) => {
dispatch({
type: 'API',
payload: {
url: 'Papers/addPaper',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
},
data: data,
onSuccess: (response) => {
onFetchAddPaper(response);
resolve(response); //return your promise on success
},
onError: (error) => {
onFetchAddPaper(error);
reject(error); //return your promise on failure
}
}
});
})
};
};
So, whenever your action executes, it'll be a promise which you can then evaluate like -
this.props.fetchAddPaper(data).then(response => {..do something})

Resources