Im facing an issue now with my authentication system that is based on django-simplejwt and redux-saga.
Whenever i click on the log out button , the following saga is run:
export function* workerLogout(action) {
const r_token = localStorage.getItem('refresh_token');
yield call(() => axios.post('http://127.0.0.1:8000/api/blacklist/', {
"refresh_token": r_token
})) //<--- API end-point to blacklist the tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token'); // <--- removing the tokens
yield call(() => axiosInstance.defaults.headers['Authorization'] = null ); //<--- setting the headers to null
yield put({ type: "LOGOUT_SUCCESS" }) //<---- This will go to the reducer to set the states to null
}
Which will then acccess the reducer here:
const authLogout = (state, action) => {
return updateObject(state, {
access_token: null,
refresh_token: null,
isAuthenticated: false,
group: {},
username: null,
});
}
This works and my redux tool bar shows that the state of the tab is indeed cleared:
isAuthenticated(pin):false
access_token(pin):null
refresh_token(pin):null
group(pin):
username(pin):null
My interceptor:
axiosInstance.interceptors.response.use(
response => {
return response
},async error => {
const originalRequest = error.config;
// Prevent infinite loops
if (error.response.status === 401 && originalRequest.url === '/token/refresh/') {
store.dispatch({type: "LOGOUT"});
return Promise.reject(error);
}
if (error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized")
{
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken){
const tokenParts = JSON.parse(atob(refreshToken.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
console.log('access token is expired , attempting refresh')
try {
const response = await axiosInstance
.post('/token/refresh/', { refresh: refreshToken });
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
axiosInstance.defaults.headers['Authorization'] = "JWT " + response.data.access;
originalRequest.headers['Authorization'] = "JWT " + response.data.access;
axiosInstance.get('group/get/').then(groups => {
store.dispatch({type: "LOGIN_SUCCESS" , payload:[response.data.access, refreshToken, groups.data, jwt_decode(response.data.access).username]});
})
return axiosInstance(originalRequest);
}
catch (err) {
store.dispatch({type: "GET_ERRORS" , error : err.response.data})
}
}else{
store.dispatch({type: "LOGOUT"});
}
}
}
store.dispatch({type: "GET_ERRORS" , error : error.response.data})
return Promise.reject(error);
}
);
However , if let's say a second tab was open at the point of the logout , i can still access my server's resources from the second tab. Upon closer inspection of the redux tool bar , it seems the state of the second tab is not affected by the first tab.
Is this an expected behavior? If so how do i make it in synced across all tabs as this seems like a bad way to secure the application.
I would suggest to store your Accesstoken and refreshtoken in Browser's local storage. Clearing accesstoken from redux store will not impact the other tabs.
On your Login success you can store accesstoken as
static saveToken(token, refreshToken){
window.localStorage.setItem('token', token);
window.localStorage.setItem('refresh_token', refreshToken);
}
On your logout success you can just remove the token by
static clearAccessToken(){
window.localStorage.removeItem('token');
window.localStorage.removeItem('refresh_token');
}
In your request Interceptor you can access the accesstoken/refreshtoken by
static isAuthenticated(){
return window.localStorage.token != null;
}
static getAccessToken(){
return window.localStorage.token;
}
static getRefreshToken(){
return window.localStorage.refresh_token;
}
By doing this way when you logout from one tab it will logged out from your browser.
You can contorl the logout error in authInterceptor
axios.interceptors.response.use((response) => {
return response
}, (error) => {
const { config, response: { status } } = error;
const originalRequest = config;
if (status === 401) {
if(config.url.endsWith('oauth/token') || config.url.endsWith('users/logout')){
return Promise.reject(error);
}
if (!this.isAlreadyFetchingAccessToken) {
this.isAlreadyFetchingAccessToken = true;
var refreshTokenValue = AuthService.getRefreshToken();
refreshToken(refreshTokenValue).then((token) => {
this.isAlreadyFetchingAccessToken = false
if(token && token.data && token.data.access_token){
AuthService.saveToken(token.data.access_token, token.data.refresh_token);
this.onAccessTokenFetched(token.data.access_token);
}
}).catch(err => {
this.isAlreadyFetchingAccessToken = false;
this.subscribers = [];
this.history.push('/login');
return Promise.reject("Auth error");
})
}
const retryOriginalRequest = new Promise((resolve) => {
this.addSubscriber(access_token => {
originalRequest.headers.Authorization = 'Bearer ' + access_token
resolve(axios(originalRequest))
})
})
return retryOriginalRequest
}
console.log("Unknown error :" + error);
let errorMessage = '';
if(error.response && error.response.data) {
return Promise.reject(error.response.data);
} else {
return Promise.reject({
code: UNKNOWN_ERROR,
message: "Oops, Unable to complete the request"
});
}
})
This is my request interceptor
axios.interceptors.request.use(function (config) {
if(AuthService.isAuthenticated()){
if(config.url.startsWith(apiUrl) && !config.url.startsWith(OAUTH_URL)){
config.headers.Authorization = 'Bearer ' + window.localStorage.token;
}
}
return config;
});
In the above code I am reading my access token directly from localstorage. Upon logout the Authorization header by default becomes invalid and throws 401 now In your reponse interceptor you can redirect to logout page when refresh token fails. Hope this will help.
Related
I am using an Axios interceptor (in React) to retry on 401 (when my access token expires). I want to limit to one retry but for some reason I'm unable to read the retried property I am defining.
This is the code I am using in the interceptor.
const responseIntercept = axios.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
console.log(prevRequest);
console.log(prevRequest.retried);
if (error?.response?.status === 401 && !prevRequest?.retried) {
await new Promise(r => setTimeout(r, 1500)); // only here to delay the infinite retries
prevRequest.retried = true;
// log here returns true
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = newAccessToken;
return axios(prevRequest);
}
return Promise.reject(error);
}
);
For some reason, logging of prevRequest shows an object with the property retried, but the second log of .retried always logs 'undefined'. I assume this is the problem but I have no idea why I can see the property set but can't access it.
If I log prevRequest after adding the property, it does return true.
console log
Edit (solution): After taking bogdanoff's advice, this is the working solution I ended up with:
const NO_RETRY_HEADER = 'x-no-retry'
...
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
var prevRequest = error?.config;
if (error?.response?.status === 401 && prevRequest?.headers[NO_RETRY_HEADER] == null) {
// get new token, return error if refresh errors
try {
const newAccessToken = await refresh(controller.signal);
// retry with new token
prevRequest.headers[NO_RETRY_HEADER] = 'true';
prevRequest.headers['Authorization'] = newAccessToken;
return axiosPrivate(prevRequest);
} catch (error) {/* no-op */}
}
return Promise.reject(error);
}
);
I have been there recently, I used headers instead of modifying config.
const NO_RETRY_HEADER = 'x-no-retry'
const responseIntercept = axios.interceptors.response.use(undefined, async (error) => {
if (!axios.isCancel(error) && axios.isAxiosError(error) && error.response.status === 401) {
if (error.config.headers && error.config.headers[NO_RETRY_HEADER]) {
return Promise.reject(error)
}
error.config.headers ||= {}
error.config.headers[NO_RETRY_HEADER] = 'true' // string val only
const newAccessToken = await refresh()
error.config.headers['Authorization'] = newAccessToken
return axios(error.config)
}
return Promise.reject(error)
})
I am developing react project using JWT authentication and write this code.
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000
});
service.interceptors.response.use( (response) => {
return response.data
}, async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN)
history.push(ENTRY_ROUTE)
window.location.reload();
}
// Unauthorized error token should refresh with access token
if (error.response.status === 401) {
const jwtToken = localStorage.getItem(AUTH_TOKEN)
const token = JSON.parse(jwtToken)
const refresh_token = token['refresh'];
const tokenParts = JSON.parse(atob(refresh_token.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post('/authentication/token/refresh/', {refresh: refresh_token});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
}
return Promise.reject(error);
});
This is working well and have no errors, but I have something to ask. When the token is expired and some API is called, I get 401 status code to get access token with refresh token. And it returns the access token correctly.
But there is no way to re-call the failed API ( just called with expired access token ). That's why there are cases I could have no response from the backend ( Imagine, user clicks button, but no response, so he should click the button again to see the response. This is because of calling with new access token again )
This code is used in all parts of the project and I have many API callings, so it seems impossible to re-call failed API in each react components.
How can I fix this problem?
Best bet is to invoke axios again with the original request (with new token). I haven't tried setting up JWT server & executing this scenario, but something like below should work:
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000,
});
service.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
if (error.response.status != 401) return Promise.reject(error);
// Unauthorized error token should refresh with access token
const jwtToken = localStorage.getItem(AUTH_TOKEN);
const token = JSON.parse(jwtToken);
const refresh_token = token["refresh"];
const tokenParts = JSON.parse(atob(refresh_token.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post("/authentication/token/refresh/", {
refresh: refresh_token,
});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
axios
.request(originalRequest)
.then((response) => {
Promise.resolve(response);
})
.catch((err) => {
Promise.reject(err);
});
//return service(originalRequest);
} catch (error) {
Promise.reject(error);
}
} else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
Promise.reject(error);
}
}
);
Hope this helps 👍
After some days of investigating my code, I finally found the answer.
Let me write my code and hope it will be helpful.
let refreshToken;
...
if (tokenParts.exp > now) {
try {
if (!refreshToken) {
refreshToken = service.post('/authentication/token/refresh/', {refresh: refresh_token}).then(token => {
refreshToken = null;
return token;
});
}
return refreshToken.then(res => {
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.defaults.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
})
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
This prevents multiple token refresh API request and surely made the failed API call again with new access token.
I'm using React.useEffect() to retrieve the users list.
React.useEffect(() => {
dispatch(UsersActions.creators.fetchingUsersAction());
UsersApi.methods.getUsers().then(
(res) => {
dispatch(UsersActions.creators.fetchUsersSuccessAction(res.data));
},
(e) => {
dispatch(UsersActions.creators.fetchUsersErrorAction());
}
);
}, [dispatch]);
On this example, fetchingUsersAction is used to set "loading" to true, and fetchUsersErrorAction to false. This works fine, except when the request fails due to token expiration.
ApiClient.interceptors.response.use(
function (response) {
return response;
},
function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refresh = JSON.stringify({
refreshToken: localStorage.getItem("refresh"),
});
AuthenticationApi.methods.refresh(refresh).then((res) => {
if (res.data.accessToken) {
localStorage.setItem("token", res.data.accessToken);
}
ApiClient.defaults.headers.common["Authorization"] =
"Bearer " + res.data.accessToken;
originalRequest.headers["Authorization"] =
"Bearer " + res.data.accessToken;
return ApiClient(originalRequest);
});
}
return Promise.reject(error);
}
);
This is sending a request to generate a new token and the previous request, but since the first request failed, the useEffect is going to the error section, making the "loading" false and showing the users list based on the previous state. What is the best way to deal with this problem?
Thanks
You should create an Async fucntion inside useEffect hook and use await to wait for the response, then call the function. Here is one example:
useEffect(() => {
const getRoles = async () => {
await authService.roles().then((res) => {
//Do your stuff.
console.log(res);
}).catch((error) => {
console.log(`'Catching the error: '${error}`);
});
};
//Call the recent created function.
getRoles();
}, []);
Your interceptor looks good to me.
my axios interceptor is:-
axios.interceptors.response.use((response, error) => {
const originalRequest = response.config;
if (response.data.status === 'Token is Expired' && originalRequest.url === '/api/refresh') {
this.props.history.push('/logout');
Promise.reject(error);
}
if (response.data.status === 'Token is Expired' && !originalRequest._retry) {
originalRequest._retry = true;
const playerToken = localStorage.getItem('accessToken');
return axios
.get('/api/refresh', {
headers: {
Authorization: `bearer ${playerToken}`,
},
})
.then(res => {
console.log('from refreshtoken', res);
const stringRes = JSON.stringify(res);
const parsedRes = JSON.parse(stringRes);
const stringData = JSON.stringify(parsedRes.data);
const parsedData = JSON.parse(stringData);
const stringToken = JSON.stringify(parsedData.data);
const parsedToken = JSON.parse(stringToken);
if (parsedData.success == true) {
localStorage.setItem('accessToken', playerToken);
axios.response.config.headers['Authorization'] = `bearer ${parsedToken}`;
return Promise.resolve();
return axios(originalRequest);
} else {
this.props.history.push('/logout');
}
})
.catch(err => {
console.log('from refreshtoken', err);
});
}
return Promise.reject(error);
});
My code is running but when my refresh token API is called first time, It also returns the same status "Token is expired" due to which i am logged out of the app. This is happening only in interceptor. When i am calling Refresh API outside of interceptor, it returns with a refresh token.
Does my code have mistakes? or it is some other coding fault entirely.
Please answer & tell me the right way to do it & where do i place my interceptor??
Currently it is placed in a Component which is called just after login.
Usually the flow should be as such:
making a regular request with accessToken
request fails with status code 401
axios interceptor catches it and makes request to token/refresh. from that response it gets a new access token.
retries the original request.
So the code should looks like this (this is a working example from my app):
function isUnAuthorizedError(error) {
return error.config && error.response && error.response.status === 401;
}
function shouldRetry(config) {
return config.retries.count < 3;
}
function updateAuthToken(response) {
localStorage.setItem('token', response.data.accessToken);
}
async function authInterceptor(error) {
error.config.retries = error.config.retries || {
count: 0,
};
if (isUnAuthorizedError(error) && shouldRetry(error.config)) {
const response = await axios.post(`/token/refresh`, {});
updateAuthToken(response);
error.config.retries.count += 1;
axios.defaults.headers.common.Authorization = `Bearer ${response.data.accessToken}`; // update the accessToken
return axios.rawRequest(error.config); // retries the original request
}
return Promise.reject(error);
}
axios.interceptors.response.use(null, authInterceptor); // This indicates that authInterceptor will work only on request errors (status code >= 400)
Hopes this flow makes more sense.
const store = configureStore();
let isLogout = false;
const handleResponse = (response) => {
if (response && response.data && response.data.status && response.data.status.code === 401 && !isLogout ) {
isLogout = true;
store.getState().Login.isAuthenticated= false;
store.dispatch(actions.logout());
}
return response
}
axiosApi.interceptors.response.use(
response => handleResponse(response)
)
```
const initial_state = {
userName: 'Name',
isAuthenticated: localStorage.getItem('isAuthenticated') ? true : false
};
export default function loginReducers (state, action) {
if (typeof state === 'undefined') {
return initial_state;
}
const payload = action.payload || action;
switch (action.type) {
case loginConstants.LOGOUT_SUCCESS:
return {
initial_state,
isAuthenticated: false
};
case loginConstants.SET_IS_AUTHENTICATED:
return {
isAuthenticated: payload.isAuthenticated
};
default:
return state;
}
}
```
I have mention my login action , login reducer also.
const userObj = JSON.parse(localStorage.getItem('authResponse'))
const userId = 'abcd'
const config = {};
config.url = 'abc/logout?userId=' + userId;
config.headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
dispatch(blockUI());
ApiCall.getCall(config).then((response) => {
if (response && response.status) {
if ((response.status.code === 200 || response.status.code === 201)) {
history.push('/login');
localStorage.clear();
dispatch({ type: LoginConstants.LOGOUT_SUCCESS });
}
}
dispatch(unblockUI());
});
})
I have mention my login action , login reducer also.how to change redux state using store.getstate(). I want when i got 401 error code then application will logout. But in my case , logout api call but it is not logout properly
In axios docs it has this info, so you need to handle second part for non 2xx errors, in your case 401.
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
Assuming your 401 code in error.status.code:
axiosApi.interceptors.response.use(
function(response) {
return response;
},
function(error) {
if (error.status.code === 401 ) {
store.dispatch(actions.logout());
}
return Promise.reject(error);
}
);
If you use token, in your logout action, you had better to remove it.