I am working on login feature and have problem when refresh token.
When token expire making request to refresh token, remove the old token, and save the new token to AsyncStorage.
After login successfully have to function A and B. The function A is using the new token to make its request. the function B say that it need to refresh the token so make request to refresh token ( the request make successfully, token being refresh) but The token that request A is using now invalid - I think it happens due to asynchronous
This is my code that use to refresh token:
axiosInstance.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
if (error.response.status === CODE_TOKEN_EXPIRED) {
try {
const token = await authenticationService.getRefreshToken();
const response = await authenticationService.refreshToken(token);
await authenticationService.removeToken();
await authenticationService.storeToken(response.data.params.access_token);
await authenticationService.storeRefreshToken(response.data.params.refresh_token);
error.config.headers.Authorization = 'Bearer ' + response.data.params.access_token;
error.response.config.headers['Authorization'] = 'Bearer ' + response.data.params.access_token;
return axiosInstance(error.config);
} catch (err) {
console.log(2, err);
await authenticationService.removeToken();
navigationService.navigate('LoginForm');
}
}
return Promise.reject(error);
}
);
Anyone know how to handle which asynchronous call for refresh token?
First would be for you to check if you are changing token to the correct axios instance. It is necessary to change Authorization header on error.response config as you did, but also for main axios instance (if you have one) like so: axios.defaults.headers.common["Authorization"] = "Bearer " + access_token;
If it is multiple parallel requests going on that could possibly need to be postponed after token is refreshed issue and answer gets complex, but check this gist with full refresh logic with axios.
I have implemented the same scenario in fetch API. you can also do this same in axios API. Try this to avoid interceptor concept.
Api.ts
export const api = ({ method, url, body, isProtected = true }) => {
return new Promise((resolve, reject) => {
const payload = {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
};
if (body !== null) {
(payload as any).body = JSON.stringify(body);
}
/**
* "isProtected" is used for API call without authToken
*/
if (isProtected) {
AsyncStorage.getItem(ACCESS_TOKEN).then(accessKey => {
(payload.headers as any).Authorization = `Bearer ${accessKey}`;
fetch(url, payload)
.then((response: any) => {
/*
* 419 status denotes the timeout of authToken
*/
if (response.status == 419) {
// refresh token
AsyncStorage.getItem(REFRESH_TOKEN).then(refreshKey => {
const payloadRef = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + refreshKey
}
};
/*
* This call refresh the authToken using refreshing call to renew the authToken
*/
fetch(URL.baseUrl + "/refresh", payloadRef)
.then((response: any) => response.json())
.then(response => {
/*
* if refresh token expired. redirect to login page
*/
if (response.status !== codes.SUCCESS) {
if (!User.sessionOver) {
User.sessionOver = true;
Alert.alert(
'Alert',
'Session Timeout',
[
{
text: 'Get back to Login',
onPress: () => {
// get to Login page
}
}
],
{ cancelable: false }
);
}
} else if (response.status == codes.SUCCESS) {
/*
* If refresh token got refreshed and set it as authToken and retry the api call.
*/
AsyncStorage.setItem(ACCESS_TOKEN, response.payload.access_key).then(() => {
(payload.headers as any).Authorization = 'Bearer ' + response.payload.access_key;
fetch(url, payload)
.then(response => response.json())
.then(response => {
if (response.status == codes.SUCCESS) {
resolve(response);
}
})
.catch(error => {
reject(error);
});
});
}
});
});
} else {
resolve(response.json());
}
})
.catch(error => {
reject(error);
});
});
} else {
fetch(url, payload)
.then((response: any) => {
response = response.json();
resolve(response);
})
.catch(error => {
reject(error);
});
}
});
};
MovieService.ts
import { api } from '../services/api';
import { URL } from '../config/UrlConfig';
const getMovies = () => {
const method = 'GET';
const url = URL.baseUrl + '/v1/top/movies';
const body = null;
const isProtected = true;
return api({ method, url, body, isProtected });
};
export { getMovies };
Maybe it will helps - https://gist.github.com/ModPhoenix/f1070f1696faeae52edf6ee616d0c1eb
import axios from "axios";
import { settings } from "../settings";
import { authAPI } from ".";
const request = axios.create({
baseURL: settings.apiV1,
});
request.interceptors.request.use(
(config) => {
// Get token and add it to header "Authorization"
const token = authAPI.getAccessToken();
if (token) {
config.headers.Authorization = token;
}
return config;
},
(error) => Promise.reject(error)
);
let loop = 0;
let isRefreshing = false;
let subscribers = [];
function subscribeTokenRefresh(cb) {
subscribers.push(cb);
}
function onRrefreshed(token) {
subscribers.map((cb) => cb(token));
}
request.interceptors.response.use(undefined, (err) => {
const {
config,
response: { status },
} = err;
const originalRequest = config;
if (status === 401 && loop < 1) {
loop++;
if (!isRefreshing) {
isRefreshing = true;
authAPI.refreshToken().then((respaonse) => {
const { data } = respaonse;
isRefreshing = false;
onRrefreshed(data.access_token);
authAPI.setAccessToken(data.access_token);
authAPI.setRefreshToken(data.refresh_token);
subscribers = [];
});
}
return new Promise((resolve) => {
subscribeTokenRefresh((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(axios(originalRequest));
});
});
}
return Promise.reject(err);
});
export default request;
Related
I'm trying to store and get data that I fetch from an API. The user is supposed to get a token on the login screen, and the token will be shown in an Alert dialog on home screen when the user press a button. But the token is not shown in the Alert dialog. the token is shown after I reload(not refresh the app. I used Live Server extension) the screen three times.
Login.js
const _userLogin = () => {
fetch(URLs._login, {
method: "POST",
headers, body,
})
}).then((response) => response.json())
.then((result) => {
if(result.message !== "Unauthorized / Access Token Expired" && result.message !== "The given data was invalid."){
storeData(result.access_token, result.token_type);
navigation.navigate('HomeScreen');
} else {
Alert.alert("Error", result.message);
}
});
};
const storeData = async (accessToken, tokenType) => {
try {
await AsyncStorage.setItem('#access_token', accessToken);
await AsyncStorage.setItem('#token_type', tokenType);
await AsyncStorage.setItem('#user_auth', tokenType + " " + accessToken);
} catch (e) {
console.log(e);
}
}
Home.js [UPDATE]
const [inputs, setInputs] = React.useState({
userToken: '',
userPointsBalance: '',
expiringOn: '',
});
useEffect (() => {
_dashboard();
})
const getToken = async () => {
inputs.userToken = await AsyncStorage.getItem('#user_auth');
}
const _dashboard = () => {
getToken();
fetch(URLs._dashboard, {
method: "GET",
headers: {
'Authorization': inputs.userToken,
'Content-Type': 'application/json',
},
}).then((response) => response.json())
.then(async (result) => {
storeData(result.code, result.name, result.member_name, result.user_points_balance, result.expiring_on, result.status, result.token_id);
getData();
});
};
const storeData = async (code, name, memberName, userPointsBalance, expiringOn, status, tokenId) => {
try {
await AsyncStorage.setItem('#user_points_balance', userPointsBalance.toString());
await AsyncStorage.setItem('#expiring_on', expiringOn.toString());
} catch (e) {
console.log(e);
}
}
const getData = async () => {
const userPointsBalance = await AsyncStorage.getItem('#user_points_balance');
const expiringOn = await AsyncStorage.getItem('#expiring_on');
setInputs({userPointsBalance: userPointsBalance, expiringOn: expiringOn});
}
return (
<Text>{inputs.expiringOn}<Text>
)
i hope it works
.then(async(result) => {
if(result.message !== "Unauthorized / Access Token Expired" && result.message !== "The given data was invalid."){
await storeData(result.access_token, result.token_type)
.then(res=>
navigation.navigate('HomeScreen')
)
} else {
Alert.alert("Error", result.message);
}
});
I have a separate fetch request function that logins user and saves auth token to localStorage, then my data request fetch should be send with that saved token bearer, but data fetch doesn't wait for token and receives Unauthorized access code.
My data request fetch looks like this :
// to check for fetch err
function findErr(response) {
try {
if (response.status >= 200 && response.status <= 299) {
return response.json();
} else if (response.status === 401) {
throw Error(response.statusText);
} else if (!response.ok) {
throw Error(response.statusText);
} else {
if (response.ok) {
return response.data;
}
}
} catch (error) {
console.log("caught error: ", error);
}
}
const token = JSON.parse(localStorage.getItem("token"));
// actual fetch request
export async function getData() {
const url = `${URL}/data`;
var obj = {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + `${token}`,
},
};
const data = await fetch(url, obj)
.then((response) => findErr(response))
.then((result) => {
return result.data;
});
return data;
}
My fetch requests are in a different js file, I'm importing them in my components like this:
import React, { useState, useEffect } from "react";
function getInfo() {
const [info, setInfo] = useState()
const importGetDataFunc = async () => {
const data = await getData();
setInfo(data);
};
useEffect(() => {
importGetDataFunc();
}, []);
return (
<div>
</div>
)
}
export default getInfo
Now when I go to the getInfo component after login at first fetch request returns 401, but after I refresh the page fetch request goes with token bearer and data gets returned. My problem is that I don't know how to make getData() fetch request to wait until it gets token from localStorage or retry fetch request on 401 code. I tried to implement if statement like this
useEffect(() => {
if(token){
importGetDataFunc();
}
}, []);
where useEffect would check if token is in localStorage and only then fire fetch request, but it didn't work. Any help on how I can handle this would be greatly appreciated.
You are close. You need to add token as a dependency to your useEffect. Also, you need to move your token fetching logic into your component.
Something like this should work:
import React, { useState, useEffect } from "react";
function getInfo() {
const [info, setInfo] = useState()
const token = JSON.parse(localStorage.getItem("token"));
const importGetDataFunc = async () => {
const data = await getData();
setInfo(data);
};
useEffect(() => {
if(token) {
importGetDataFunc(token);
}
}, [token]);
return (
<div>
</div>
)
}
export default getInfo
You can also modify your importGetDataFunc to receive the token as a parameter.
const importGetDataFunc = async (token) => {
const data = await getData(token);
setInfo(data);
};
export async function getData(token) {
const url = `${URL}/data`;
var obj = {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + `${token}`,
},
};
const data = await fetch(url, obj)
.then((response) => findErr(response))
.then((result) => {
return result.data;
});
return data;
}
What actually helped me is to make a function to check for a token inside get fetch request, like this:
export const findToken = () => {
const token =localStorage.getItem("token")
return token;
};
export async function getData(token) {
const url = `${URL}/data`;
var obj = {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + `${findToken()}`,
},
};
const data = await fetch(url, obj)
.then((response) => findErr(response))
.then((result) => {
return result.data;
});
return data;
}
I'm trying to implement refresh-token from react. I'm using this library axios-auth-refresh which seems to work very fine except for one API.
// api.js
import Axios from "axios";
import Cookies from 'js-cookie'
import { TOKEN_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME } from '../constants/constants';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
const api = Axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const refreshAuthLogic = async (failedRequest) => {
const refresh_token = Cookies.get(REFRESH_TOKEN_COOKIE_NAME);
// if(!refresh_token) return;
console.log(refresh_token);
const tokenRefreshResponse = await api.post('auth/createtoken', {
}, {
headers: {'Authorization': 'Bearer ' + refresh_token},
validateStatus: () => true
});
console.log(tokenRefreshResponse);
if(tokenRefreshResponse.data.statusCode === 401 || tokenRefreshResponse.data.statusCode === 403) {
Cookies.remove(REFRESH_TOKEN_COOKIE_NAME);
if(!window.location.href.includes('login')) {
window.location.href = "http://localhost:3000/login";
}
return;
}
const access_token = tokenRefreshResponse.data.access_token;
Cookies.set(TOKEN_COOKIE_NAME, access_token, { expires: 60 })
api.defaults.headers.Authorization = `Bearer ${access_token}`
failedRequest.response.config.headers['Authorization'] = 'Bearer ' + access_token;
}
// Instantiate the interceptor (you can chain it as it returns the axios instance)
createAuthRefreshInterceptor(api, refreshAuthLogic);
export default api;
The following api call does NOT repeat in case 401 is returned:
const fetchUsers = async () => {
const { data } = await api.get(`users/`, {params: {tripUsers: true}}, {
validateStatus: (status) => status !== 401 && status !== 403
})
setUsers(data);
}
useEffect(() => {
fetchUsers();
}, [])
The following api call DOES repeat in case 401 is returned:
const fetchProfile = async () => {
const { data } = await api.get(`/users/${user.userId}`, {}, {
validateStatus: (status) => status !== 401 && status !== 403
})
const {statusCode, message} = data;
console.log(data);
if(!statusCode) {
console.log(data);
setState(data);
}
}
useEffect(() => {
fetchProfile();
}, [])
Please help.
After spending some time on this issue, I decided to create a generic API caller rather than using axios interceptors or any other library. Here's my generic axios API caller. It can still be improved, but the idea is to call the API again with a new token if the first token is expired.
// api.js
import Axios from "axios";
import Cookies from 'js-cookie'
import { TOKEN_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME } from '../constants/constants';
const api = Axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
export const callApi = async (method, url, params, other) => {
const validateStatus =
url === 'auth/login' ? () => true : (status) => status !== 401 && status !== 403
const options = {
url,
method,
validateStatus,
...other
}
options[method === 'GET' ? 'params' : 'data'] = params;
console.log(options);
try {
const data = await api(options);
return Promise.resolve(data);
} catch (err) {
console.log(err.response.status);
if (err && err.response && err.response.status === 401) {
return performTokenRefresh(options);
} else {
return Promise.reject(err);
}
}
};
const performTokenRefresh = async (options) => {
const refresh_token = Cookies.get(REFRESH_TOKEN_COOKIE_NAME);
if(!refresh_token) return {};
const tokenRefreshResponse = await api.post('auth/createtoken', {
}, {
headers: {'Authorization': 'Bearer ' + refresh_token},
validateStatus: () => true
});
if(tokenRefreshResponse.data.statusCode === 401 || tokenRefreshResponse.data.statusCode === 403) {
Cookies.remove(REFRESH_TOKEN_COOKIE_NAME);
if(!window.location.href.includes('login')) {
window.location.href = "http://localhost:3000/login";
}
return {};
}
const access_token = tokenRefreshResponse.data.access_token;
Cookies.set(TOKEN_COOKIE_NAME, access_token, { expires: 60 })
api.defaults.headers.Authorization = `Bearer ${access_token}`
return api(options);
}
export default api;
hello i have this class when i call Auth.isAuthenticated() from another component in react, it always return false (its the default value), even if the server return a 200 response, witch sets this.authenticated = true .
how do i use promises to make the method wait till the fetch call is finished then return the result
Edit:
i need the Boolean true or false to be returned so based on that, i can show or hide the component, all answers are helpful but i need a Boolean not a promise any help please
class Auth {
constructor() {
this.authenticated = false;
}
isAuthenticated() {
//get token from local storage if there is one
const jwttoken = localStorage.getItem('jwttoken');
const bearer = 'Bearer ' + jwttoken;
const data = new FormData();
// get the website backend main url from .env
const REACT_APP_URL = process.env.REACT_APP_URL
fetch(`${REACT_APP_URL}/api/auth/verify`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': bearer,
},
body: data
}).then(
(response) => {
response.json()
.then((res) => {
if (response.status === 200) {
this.authenticated = true;
}
if (response.status === 401) {
localStorage.removeItem('jwttoken');
this.authenticated = false;
}
})
}
).catch((err) => {
// console.log(err)
});
return this.authenticated;
}
}
export default new Auth();
and from another component i call Auth.isAuthenticated() === true
export const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={(props) => (
Auth.isAuthenticated() === true
? <Component {...props} />
: <Redirect to='/admin' />
)} />
)
}
Let's say you want to write a function that returns a promise and resolves when some action finishes (an API call for example). You could write something like this:
const myAsyncFunction = () =>{
return new Promise((resolve, reject) =>{
//Faking an API Call
setTimeout(() => resolve('data'), 400)
})
}
There you go! Now you have a function that returns a promise which will resolve in 400ms. Now you need to use either .then() method or async await statements.
const sideEffects = async () =>{
const result = await myAsyncFunction()
console.log(result) //'data'
}
If you don't want to do async/await you can have isAuthenticated return a promise.
isAuthenticated() {
return new Promise((resolve, reject) => {
//get token from local storage if there is one
const jwttoken = localStorage.getItem('jwttoken');
const bearer = 'Bearer ' + jwttoken;
const data = new FormData();
// get the website backend main url from .env
const REACT_APP_URL = process.env.REACT_APP_URL
fetch(`${REACT_APP_URL}/api/auth/verify`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': bearer,
},
body: data
}).then(
(response) => {
response.json()
.then((res) => {
if (response.status === 200) {
resolve(true)
}
if (response.status === 401) {
localStorage.removeItem('jwttoken');
resolve(false)
}
})
}
).catch((err) => {
// reject(err)
});
})
}
And inside an async function you could do let isAuthenticated = await isAuthenticated() or you can use .then and .catch outside of async function to return result
use await async
async isAuthenticated() {
//get token from local storage if there is one
const jwttoken = localStorage.getItem('jwttoken');
const bearer = 'Bearer ' + jwttoken;
const data = new FormData();
// get the website backend main url from .env
const REACT_APP_URL = process.env.REACT_APP_URL
const response = await fetch(`${REACT_APP_URL}/api/auth/verify`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': bearer,
},
body: data
});
const responseToJson = await response.json();
if (responseToJson.status === 200) {
this.authenticated = true;
}
if (responseToJson.status === 401) {
localStorage.removeItem('jwttoken');
this.authenticated = false;
}
return this.authenticated;
}
So I've 2 components, Token.jsx and httpRequest.jsx.
Both of them call each other when the token expires and it goes into an infinite loop, I want to break the loop after 3 http erros.
export default function GenerateToken() {
return new Promise(((resolve) => {
const refreshToken = GetRefreshToken();
const url = `${cloudFunctionURL}/users/auth/idtoken/refresh`;
const headers = {
'Content-Type': 'application/json',
};
const params = {
refreshToken,
};
httpRequest.makeHTTPCall('post', url, headers, params).then((tokenObject) => {
// Storing the idToken in localstorage
// reactLocalStorage.set('PL_IdToken', tokenObject.idToken);
StoreIdToken(`Id ${tokenObject.id_token}`);
StoreUserId(tokenObject.user_id);
StoreRefreshToken(tokenObject.refresh_token);
resolve(tokenObject.id_token);
});
}));
}
// 2nd File
function makeHTTPCall(method, url, headers, params = null) {
return new Promise((resolve, reject) => {
headers.Authorization = GetIdToken();
headers['Content-Type'] = 'application/json';
qwest.setDefaultDataType('json');
qwest.setDefaultOptions({
headers,
});
// Make http request
qwest[`${method}`](url, params)
.then((xhr, response) => {
resolve(response);
})
.catch((error, xhr) => {
if (xhr.status === 401) { // IdToken expired
GenerateToken().then(() => {
resolve(GET(url));
});
}
else {
reject(error); // error
}
});
});
}
You can give both of them a parameter like tryCount, and increment it in one of the functions, rejecting once it reaches 2:
export default function GenerateToken(tryCount = 0) {
return new Promise((resolve) => {
const refreshToken = GetRefreshToken();
const url = `${cloudFunctionURL}/users/auth/idtoken/refresh`;
const headers = {
'Content-Type': 'application/json',
};
const params = {
refreshToken,
};
httpRequest.makeHTTPCall('post', url, headers, params, tryCount).then((tokenObject) => {
// Storing the idToken in localstorage
// reactLocalStorage.set('PL_IdToken', tokenObject.idToken);
StoreIdToken(`Id ${tokenObject.id_token}`);
StoreUserId(tokenObject.user_id);
StoreRefreshToken(tokenObject.refresh_token);
resolve(tokenObject.id_token);
});
});
}
// 2nd File
function makeHTTPCall(method, url, headers, params, tryCount) {
return new Promise((resolve, reject) => {
headers.Authorization = GetIdToken();
headers['Content-Type'] = 'application/json';
qwest.setDefaultDataType('json');
qwest.setDefaultOptions({
headers,
});
// Make http request
qwest[`${method}`](url, params)
.then((xhr, response) => {
resolve(response);
})
.catch((error, xhr) => {
if (xhr.status === 401 && tryCount <= 1) { // IdToken expired, and we've recursed less than twice so far
GenerateToken(tryCount + 1).then(() => {
resolve(GET(url));
});
} else {
reject(error); // error
}
});
});
}
Also note that you should avoid the explicit Promise construction antipattern:
export default function GenerateToken(tryCount = 0) {
const refreshToken = GetRefreshToken();
const url = `${cloudFunctionURL}/users/auth/idtoken/refresh`;
const headers = {
'Content-Type': 'application/json',
};
const params = {
refreshToken,
};
return httpRequest.makeHTTPCall('post', url, headers, params, tryCount).then((tokenObject) => {
// Storing the idToken in localstorage
// reactLocalStorage.set('PL_IdToken', tokenObject.idToken);
StoreIdToken(`Id ${tokenObject.id_token}`);
StoreUserId(tokenObject.user_id);
StoreRefreshToken(tokenObject.refresh_token);
return tokenObject.id_token;
});
}
// 2nd File
function makeHTTPCall(method, url, headers, params, tryCount) {
headers.Authorization = GetIdToken();
headers['Content-Type'] = 'application/json';
qwest.setDefaultDataType('json');
qwest.setDefaultOptions({
headers,
});
// Make http request
return qwest[`${method}`](url, params)
.then((xhr, response) => {
return response;
})
.catch((error, xhr) => {
if (xhr.status === 401 && tryCount <= 1) { // IdToken expired, and we've recursed less than twice so far
return GenerateToken(tryCount + 1).then(() => {
return GET(url);
});
} else {
throw new Error(error);
}
});
}