How to refresh JWT tokens in React.js Application? - reactjs

I checked all the similar questions here but none has what I need.
I'm securing the routs in my App and sending the JWT with every request and everything is fine here.
The issue is when the JWT expires, instead of logging out the user, I need to know how to refresh that token and keep the user logged in.
Everyone is talking about creating a "Middleware" that handles that, but no one says how to create that middleware and what's in it?
So, what is the best practice in doing that? Should I check for JWT expiration date before sending any request? or should I wait for a "401" response then try to refresh the token (which I don't know how to do), or what exactly?
If anyone has a working example of such a middleware or a package or a project on Github that can help me with this it would be great.
I'm only interested in the front-end part of the process, what to send from react and what should I expect to receive and what to do with it.

If you are using Axios (which I highly recommend), you can declare your token refreshing behaviours in the response's interceptors. This will apply to all https requests made by Axios.
The process is something like
Checking if the error status is 401
If there is a valid refresh token: use it to get the access token
if there is no valid refresh token: log the user out and return
Redo the request again with the new token.
Here is an example:
axios.interceptors.response.use(
(response) => {
return response
},
(error) => {
return new Promise((resolve) => {
const originalRequest = error.config
const refreshToken = localStorage.get('refresh_token')
if (error.response && error.response.status === 401 && error.config && !error.config.__isRetryRequest && refreshToken) {
originalRequest._retry = true
const response = fetch(api.refreshToken, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh: refreshToken,
}),
})
.then((res) => res.json())
.then((res) => {
localStorage.set(res.access, 'token')
return axios(originalRequest)
})
resolve(response)
}
return Promise.reject(error)
})
},
)

your middelware should look like this block of code (as example you can use whatever you want)
/* eslint-disable */
import request from 'superagent';
function call(meta, token) {
const method = meta.API_METHOD ? meta.API_METHOD : 'GET';
let req = request(method, 'http://localhost:8000/' + meta.API_CALL);
req = req.set({ Authorization: `JWT ${token}` });
req = meta.API_TYPE ? req.type('Content-Type', meta.API_TYPE) : req.set('Content-Type', 'application/json');
if (meta.API_PAYLOAD) {
req = req.send(meta.API_PAYLOAD);
}
if (meta.API_QUERY) {
req.query(meta.API_QUERY);
}
return req;
}
export default store => next => action => {
const state = store.getState();
const token = state.logged && state.logged.get('token') ?
state.logged.get('token') : 'eyJhbGciOiJIUzUxMiJ9';
if (action.meta && action.meta.API_CALL) {
call(action.meta, token)
.then((res) => {
store.dispatch({
type: action.meta.API_SUCCESS,
result: res.body,
});
})
.catch(({ status, response }) => {
if (action.meta.API_ERRORS && action.meta.API_ERRORS[status]) {
return store.dispatch({
type: action.meta.API_ERRORS[status],
result: response.body,
});
}
if (action.meta.API_ERRORS && action.meta.API_ERRORS[status] === '401') {
/*call the refresh token api*/
call(<Your Meta for refreshing>, <expiredtoken>)
.then((res) => {
store.dispatch({
type: action.meta.API_SUCCESS,
result: res.body,
});
})
.catch(({ status, response }) => {
if (action.meta.API_ERRORS && action.meta.API_ERRORS[status]) {
return store.dispatch({
type: action.meta.API_ERRORS[status],
result: response.body,
});
}
throw response;
});
}
throw response;
});
}
return next(action);
};

Related

Axios Interceptor is not working in React JS

I am using the below code as an interceptor in my React JS app for getting token back but unfortunately, it is not working. Refresh token returns new idToken and updates local storage data correctly. The same code I'm using some other application which works fine. One main difference is that I currently use React 18 and the previous 16. I struggled to identify the problem but failed. Your help will be appreciable.
axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response.status === 401) {
// console.log(error.response.data.code)
let usersData = JSON.parse(localStorage.getItem("userData"));
const refreshToken = usersData.refreshToken;
return axios
.post(
`${api_base_url}/auth/authentication/refresh_token`,
JSON.stringify({
refresh_token: refreshToken,
})
)
.then((response) => {
usersData["accessToken"] = response.data.data.accessToken;
usersData["idToken"] = response.data.data.idToken;
setSessionStorage("userData", usersData);
error.response.config.headers[
"Authorization"
] = `Bearer ${response.data.data.idToken}`;
return axios(error.response.config);
})
.catch((error) => {
if (error.response.data.code !== "TOKEN_EXPIRED") {
return;
}
localStorage.clear();
window.location = "/login";
});
}
return Promise.reject(error);
}
);
function getIRequestProp(severType, isMultipart, isSocial) {
const serverUrl = severType ? social_api_base_url : api_base_url;
let userData = JSON.parse(localStorage.getItem('userData'));
let idToken;
idToken = userData !== null ? userData['idToken'] : '';
let content_type;
if (isSocial) {
content_type = 'application/x-www-form-urlencoded'
} else {
content_type = isMultipart ? 'multipart/form-data' : 'application/json'
}
return {
serverUrl: serverUrl,
requestHeader: {
'Content-Type': content_type,
'Accept-Language': DEFAULT_LANGUAGE,
Authorization: `Bearer ${idToken}`
}
};
}
async function post(url, body, isSocialServer, isMultipart) {
const {serverUrl, requestHeader} = getIRequestProp(isSocialServer, isMultipart);
return axios.post(serverUrl + url, body, {
headers: requestHeader
});
}
So, I call API like this:
AxiosServices.post(ApiUrlServices.SOCIALS_UPDATE_LINKS(UserInfo.userId), payload, false)
.then(response => {})
What i figured out that return axios(error.response.config); is not sending authorization token in API request headers and trying request infinitely. But consoling error.response.config shows token sets in the config correctly.
Adding an additional modification of axios request, I solved my problem.
axios.interceptors.request.use(request => {
// Edit request config
let usersData = JSON.parse(localStorage.getItem('userData'));
request.headers['Authorization'] = `${usersData.idToken}`;
return request;
}, error => {
return Promise.reject(error);
});

Axios interceptor - shows sign in form

The axios interceptor is not working correctly for me.
Here is my code for refreshing JWT tokens:
axios.interceptors.request.use(
async config => {
const value = localStorage.getItem("access_token");
console.log("Sending request: " + value);
config.headers = {
'Authorization': `Bearer ${value}`,
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
return config;
},
error => {
return Promise.reject(error)
});
// Response interceptor for API calls
axios.interceptors.response.use((response) => {
console.log("Response OK!");
return response;
},
function (error) {
console.log("Error, refreshing access token!");
const originalRequest = error.config;
if (error.response.status === 401) {
console.log("Retry: true")
originalRequest._retry = true;
let axiosInstance = axios.create();
return axiosInstance
.post('/auth/token/refresh/', {refresh: localStorage.getItem("refresh_token")})
.then(res => {
if (res.status === 200) {
console.log("Access token:" + res.data.access);
store.dispatch(setAccessToken(res.data.access));
return axios(originalRequest);
}
});
}
return Promise.reject(error);
});
The token gets refreshed correctly, however everytime I make the first request (after the access_token expired) it always shows me the browsers 'Sign In' form.
The problem was not with the interceptor, but at backend site.
Basic Authentication was enabled and it seems that browsers tries to get the auth parameters from the user ('Sign in' form) first.

Getting access token with the refresh token after expiration(JWT)

I get 401 error after a while when the page is reloaded, I figured it could be because the access token is expired. How do I set a new token with my refresh token? The below function runs every time the user visits a new page or refreshes the page. But it doesn't seem to work.
export async function currentAccount() {
if (store.get('refreshToken')) {
const query = {
grant_type: 'refresh_token',
companyId: store.get('lastCompanyId'),
refresh_token: store.get('refreshToken'),
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
})
.catch(err => {
console.log('error', err)
store.clearAll()
})
}
return false
}
Login sets the access tokens
export async function login(email, password) {
const query = {
grant_type: 'password',
username: email,
password,
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const {
data: {
access_token: accessToken,
refresh_token: refreshToken,
},
} = response
const decoded = jsonwebtoken.decode(accessToken)
response.data.authUser = decoded.authUser
const { userId, profileId, companyId } = decoded.authUser
if (accessToken) {
store.set('accessToken', accessToken)
store.set('refreshToken', refreshToken)
}
return response.data
}
return false
})
.catch(err => console.log(err))
}
saga users.js
export function* LOAD_CURRENT_ACCOUNT() {
yield put({
type: 'user/SET_STATE',
payload: {
loading: true,
},
})
const { authProvider } = yield select((state) => state.settings)
const response = yield call(mapAuthProviders[authProvider].currentAccount)
if (response) {
const decoded = jsonwebtoken.decode(response.access_token)
response.authUser = decoded.authUser
yield store.set('id', id)
try {
const user = yield call(LoadUserProfile)
if (user) {
const { company } = user
yield put({
type: 'user/SET_STATE',
payload: {
...user,
preferredDateFormat: user.preferredDateFormat || 'DD/MM/YYYY',
userId,
id,
},
})
}
} catch (error) {
}
}else{
store.set('refreshToken', response.refreshToken)
}
yield put({
type: 'user/SET_STATE',
payload: {
loading: false,
},
})
}
You can get a new access token with your refresh token using interceptors. Intercept and check for response status code 401, and get a new access token with your refresh token and add the new access token to the header.
Example:
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) { // check for the status code 401 and make call with refresh token to get new access token and set in the auth header
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
});
Simple Interceptor example,
axios.interceptors.request.use(req => {
req.headers.authorization = 'token';
return req;
});
Interceptor example for 401
axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401) {
// Fetch new access token with your refresh token
// set the auth header with the new access token fetched
}
});
There are several good posts on Interceptors usage for getting a new access token with your refresh token
https://thedutchlab.com/blog/using-axios-interceptors-for-refreshing-your-api-token
https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
Automating access token refreshing via interceptors in axios
https://stackoverflow.com/a/52737325/8370370
The above answer is good. But I found below method is better than that also using Axios Interceptors and "jwt-decode". Give it a try. (I'm using session storage for this example. You can use your own way to store the tokens securely)
Methodology
Login to get an access token and long live refresh token and then store them securely.
Create an axios instance to check the access token expiration with "jwt-decode". Then add the access token into the request if there is a valid access token, or else request a new access token using the stored refresh token and then apply the new access token into the request.
Login:
import axios from 'axios'
const handleLogin = async (login) => {
await axios
.post('/api/login', login, {
headers: {
'Content-Type': 'application/json'
}
})
.then(async response => {
sessionStorage.setItem('accessToken', response.data.accessToken)
sessionStorage.setItem('refreshToken', response.data.refreshToken)
})
.catch(err => {
if (errorCallback) errorCallback(err)
})
}
Create axios instance:
import axios from 'axios'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'
const axiosInstance = axios.create({
headers: { 'Content-Type': 'application/json' }
})
axiosInstance.interceptors.request.use(async req => {
const accessToken = sessionStorage.getItem('accessToken') ? sessionStorage.getItem('accessToken') : null
if (accessToken) {
req.headers.Authorization = `Bearer ${accessToken}`
}
const tokenData = jwt_decode(accessToken)
const isExpired = dayjs.unix(tokenData.exp).diff(dayjs()) < 1
if (!isExpired) return req
const refreshToken = sessionStorage.getItem('refreshToken')
const response = await axios.post('/api/refresh', { refreshToken }, {
headers: {
'Content-Type': 'application/json'
}
})
req.headers.Authorization = `Bearer ${response.data.accessToken}`
sessionStorage.setItem('accessToken', response.data.accessToken)
return req
})
export default axiosInstance
Use axios instance in all the requests (Redux Toolkit Example):
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
// Import axiosInstance
import axiosInstance from 'src/utils/axiosInstance'
export const getItems = createAsyncThunk(
'appItems/getItems',
async (args, { rejectedWithValue }) => {
try {
const response = await axiosInstance.get('/api/items')
return response.data
} catch ({ response }) {
return rejectedWithValue({ code: response.status, ...response.data })
}
}
)

react admin returns Unauthorized 401 error upon CRUD operations

I am working on a react-admin project. The backend is written using Django rest framework which runs on a docker container. The authentication endpoints for access and refresh tokens are written using djangorestframework-simplejwt and served at http://localhost:8000/api/token/ and http://localhost:8000/api/token/refresh/ respectively.
I have written my own authProvider.js and dataProvider.js for react admin. The login and checkAuth functions for authProvider.js looks like this
// in src/authProvider.js
import jwt from "jsonwebtoken";
export default {
login: async ({ username, password }) => {
const request = new Request('http://localhost:8000/api/token/', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
const response = await fetch(request);
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
const { refresh, access } = await response.json();
localStorage.setItem('refreshToken', refresh);
localStorage.setItem('accessToken', access);
},
logout: ...
checkAuth: async () => {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (accessToken && refreshToken) {
const { exp } = await jwt.decode(accessToken);
if (exp > (new Date().getTime() / 1000) - 10) {
return Promise.resolve();
} else {
const request = new Request('http://localhost:8000/api/token/refresh/', {
method: 'POST',
body: JSON.stringify({ "refresh": refreshToken }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
const response = await fetch(request)
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
localStorage.setItem('accessToken', token);
return Promise.resolve();
});
return response;
}
}
return Promise.reject();
},
checkError: ...
getPermissions: () => Promise.resolve(),
}
Retrieving data works fine. But whenever I perform a create, edit and delete operation, I am automatically logged out with a 401 Unauthorized error. Error message from docker server log
Unauthorized: /api/products/2
"PUT /api/products/2 HTTP/1.1" 401
Error from browser console: PUT HTTP://localhost:8000/api/products/2 401 (Unauthorized)
Prior to adding authProvider and using docker container as backend, CRUD data mutations worked fine, using a local python venv as backend. So I assume the dataProvider.js is not responsible here.
I have not been able to figure this out for quite some time. Can anyone help me figure out what I might be doing wrong here? Thank you for your time.
EDIT 1: It seems the access token is not sent from the frontend during API request, hence the server returning 401 Unauthorized
You need to modify your dataProvider to include the token (in a token, a cookie, or in a GET parameter, depending on what your backend requires). This is explained in the react-admin auth documentation:
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const { token } = JSON.parse(localStorage.getItem('auth'));
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
};
const dataProvider = simpleRestProvider('http://localhost:3000', httpClient);
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
...
</Admin>
);

How to use refresh token in reactjs

I am working on a admin app in reactjs which uses redux for state management. In my app when user log in it gets access_token and refresh _token. access_token which gets expired after 5 min. after 5 min token becomes invalid to make any api endpoint request.
I want to know how am I suppose to use this refresh_token to update my access_token which is stored in localStorage of the browser. I had no idea about this refresh_token before this. This is how I make login request and save my access_token and refresh_token.
authentication:
export const Authentication = async(payload) =>{
try{
const response = await fetch(`${generalConfig.baseURL}/oauth/token`, {
method: 'POST',
cache: 'no-cache',
headers: {
'Authorization': `Basic ${btoa("topseller:topseller")}`,
'Accept': '*/*',
// 'Content-Type': 'multipart/form-data',
'accept-encoding': 'gzip, deflate',
},
body: payload
})
.then((response)=>{
console.log(response)
return response.json()
},err=>{
console.log(err,'############')
})
console.log(response,'#########')
return response;
}catch(err){
console.log(err,'############')
}
}
authentication.action:
export const getAccessToken = (dataToSend) => async (dispatch) => {
try {
var formData = ConvertToFormData(dataToSend);
const authResponse = await Authentication(formData);
<-- See above about this Authentication function
const response = await fetch("http://api.smartocart.com/userType", {
method: "GET",
cache: "no-cache",
headers: {
Authorization: `Bearer ${authResponse.access_token}`,
},
});
const payload = await response.json();
if (payload.status === "admin") {
SaveToLocalStorage("access_token", authResponse.access_token);
SaveToLocalStorage("refresh_token", authResponse.refresh_token);
dispatch({
type: GET_ACCESS_TOKEN,
payload: {
access_token: authResponse.access_token,
refresh_token: authResponse.refresh_token,
},
});
} else {
dispatch({
type: ERROR_ACCESS_TOKEN,
buttonPressed: true,
});
}
} catch (exception) {
console.log("Log In again");
}
};
I did read about this in some of the blog post but i did get this. https://nmajor.com/posts/access-and-refresh-token-handling-with-redux
Any help would be highly appreciated.
You can add token expiry time validation on app.js so if you reload you application or move to next page or if you make api call it will check token expiry time validation always if token expired it will make call to fetch update token
check below example : i gave example with react axios
axios.interceptors.request.use(async (config) => {
const expireAt = localStorage.getItem('expiresAt');
let token = localStorage.getItem('authToken');
if (dayjs(expireAt).diff(dayjs()) < 1) {
const data = onGetForcedToken();
token = typeof data === 'string' ? data : await data();
}
// setting updated token
localStorage.setItem('authToken', token);
return config;
}, (err) => {
console.log("error in getting ",err)
});
If you are using Axios, you can intercept a request with the help of interceptor and call api to get a new token in case token got expired.
Another approach to get a new token is by periodically calling api after certain interval before the token gets expired.
For example in App.js
// Get new Token
const getNewUserToken = async () => {
const submitUrl = apiRoot + "/v1/user/refreshtoken";
try {
const res = await fetch(submitUrl, {
method: "GET",
headers: {
token: localStorage.getItem("token"),
"Content-Type": "application/json",
},
});
if (res.status === 200) {
const data = await res.json();
localStorage.setItem("token", data.token);
} else {
// New token didnt received.Remove the previous token and user
localStorage.removeItem("token");
localStorage.removeItem("user");
setUser({});
navigate("/");
}
} catch (err) {
console.log(err);
}
};
const intervalRef = useRef();
const getToken = useCallback(() => {
// Get new token if and only if existing token is available
if (localStorage.getItem("token") != null) {
getNewUserToken();
}
}, []);
// Trigger API to get a new token before token gets expired.
useEffect(() => {
const interval = setInterval(() => getToken(), 1000 * 60 * 6); // 6 minutes interval as our token will expire after 7 minutes.
intervalRef.current = interval;
return () => clearInterval(interval);
}, [getToken]);
Hope this will help you to automatically refresh the token without forcing user to login.

Resources