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;
Related
here i have bit problem with my custom useFetch
i want to call the token first and the fetch code function later. I was tried in the useEffect this way
its creating the token firstly and queryCall(tokenKey) function still taking the null token
value if the valid token available
here is the my logic in useEffect
React.useEffect(() => {
if (!tokenKey) {
const getToken = () => {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
};
getToken();
} else {
queryCall(tokenKey);
}
}, [query]);
here is the ApplicationToken function is responsible for creating new token based on the useEffect condition (!tokenKey)
ApplicationToken.js
import qs from 'qs';
import axios from 'axios';
const ApplicationToken = IsError => {
// eslint-disable-next-line no-undef
const Grant = window?._env_?.REACT_APP_GRANT;
// eslint-disable-next-line no-undef
const Client = window?._env_?.REACT_APP_CLIENT;
// eslint-disable-next-line no-undef
const Key = window?._env_?.REACT_APP_KEY;
// eslint-disable-next-line no-undef
const Auth = window?._env_?.REACT_APP_AUTH;
// eslint-disable-next-line no-undef
if (IsError || window.localStorage.getItem('applicationToken') === null) {
let data = qs.stringify({
grant_type: `${Grant}`,
client_id: `${Client}`,
client_secret: `${Key}`,
});
let config = {
method: 'POST',
url: `${Auth}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: data,
};
axios(config)
.then(response => {
window.localStorage.setItem(
'applicationToken',
JSON.stringify(response)
);
})
.catch(function (error) {
console.log(error);
});
return JSON.parse(window.localStorage.getItem('applicationToken'));
} else {
return JSON.parse(window.localStorage.getItem('applicationToken'));
}
};
export default ApplicationToken;
Im storing the access token in local storage
here is the useFetch.js
import React from 'react';
import qs from 'qs';
import axios from 'axios';
import ApplicationToken from './ApplicationToken';
var tokenKey = ApplicationToken(false);
const useFetch = query => {
const [status, setStatus] = React.useState('idle');
const [result, setResult] = React.useState([]);
const newResult = query.substituteDataValue.toLowerCase();
var data = qs.stringify({
query: `${query.api}`,
});
// eslint-disable-next-line no-undef
const Query = window?._env_?.REACT_APP_QUERY;
const queryCall = React.useCallback(
async token => {
if (!token) {
return;
} else {
setStatus('Loading');
var config = {
method: 'POST',
url: `${Query}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization:
token?.data?.token_type + ' ' + token?.data?.access_token,
},
data: data,
};
setStatus('Loading');
axios(config)
.then(response => {
// console.log(response.data.results.bindings);
setStatus('success');
setResult(response.data.results.bindings);
})
.catch(function (error) {
setStatus('error');
if (error.response.status === 401) {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
}
});
}
},
[data]
);
React.useEffect(() => {
if (!tokenKey) {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
} else {
queryCall(tokenKey);
}
}, [query]);
return [status, result];
};
export default useFetch;
i want the output process like this
step-1.create newToken if existing token fail
step-2. call the queryCall(tokenKey) with updated token tokenKey
1 & 2 are in useEffect only in my code
when im doing it, the firstStep is working fine but second step is working with older token
so the effect of result is useFetch file if(!token) return;
how does queryCall() function wait until newToken created in useEffect ?
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;
}
If I put my config as the second argument, my cancellation token (third arg) gets ignored. However I need the Authorization header because this get request will be (but isn't yet!) Behind authentication middleware on my node API. So my question is: Where do I put my config?
const config = {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
};
const getBoards = async (cancelToken) => {
try {
if (localStorage.getItem("token") == null) {
throw new Error();
}
const response = await Axios.get("/boards", {
cancelToken: cancelToken.token,
});
setBoards(response.data);
} catch (e) {}
};
useEffect(() => {
const request = Axios.CancelToken.source();
getBoards(request);
return () => {
request.cancel();
};
}, []);
pass them in the same object.
const response = await Axios.get("/boards", {
headers: {},
cancelToken: cancelToken.token,
});
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;
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;
}