Problem with JWT Refresh Token Flow with axios/axios-auth-refresh - javascript

(I've read a number of similar questions here, and most/all have said to use a different axios instance for the refresh token requests (versus the API requests). However, I'm not clear on how that would work, since I am using axios-auth-refresh for auto-refreshing the access tokens.)
I'm working on an app with a JWT-based authentication flow for back-end API requests. The general flow is working fine; upon login the user gets a long-term refresh token and short-term access token. Using the axios-auth-refresh plug-in for axios, I am able to auto-refresh the access token when it has expired.
My problem is, when the refresh token expires, I am not able to catch the error and redirect the user to re-authenticate. Nothing I've tried catches the error. The (current) code for the auto-refresh hook is:
const refreshAuth = (failed) =>
axios({ method: "post", url: "token", skipAuthRefresh: true })
.then(({ status, data: { success, accessToken } }) => {
console.warn(`status=${status}`);
if (!success) Promise.reject(failed);
processToken(accessToken);
// eslint-disable-next-line no-param-reassign
failed.response.config.headers.Authorization = `Bearer ${accessToken}`;
return Promise.resolve();
})
.catch((error) => console.error("%o", error));
createAuthRefreshInterceptor(axios, refreshAuth);
In cases of the refresh token being stale or missing, I see neither the status=xxx console line nor the dump of an error object in the catch() block.
The actual file this is in is on GitHub here, though it is slightly different than the working version above. Mainly, in the GH version the hook calls axios.post("token").then(...) where above I'm making a more explicit call to add the skipAuthRefresh parameter. Adding that got me more detailed error traces in the console, but I am still not catching the 401 response via the catch().
I've tried everything I can think of... anything jump out as something I'm missing?
Randy
(Edited to ensure the GitHub link points to the version of the file that has the issue.)

Since posting this, I have managed to work through the problem and come up with a working solution.
The key to the solution does in fact lie in using a different axios instance for the calls to renew the refresh token. I created a second module to encapsulate a second axios instance that would not get the interceptor created by the axios-auth-refresh module. After working around some inadvertent circular-dependency issues that this initially caused, I reached a point where I could see the exception being thrown by axios when the refresh token itself is stale or missing.
(Interestingly, this led to another problem: once I recognized that the refresh token was no longer valid, I needed to log the user out and have them return to the login screen. Because the application this is in is a React application, the authentication was being handled with custom hooks, which can only be called within a component. However, I had abstracted all the API calls into a non-React module so that I could encapsulate things like the addition of the Authorization header, the base URL, etc. At that level I could not run the auth hook to get access to the logout logic. I solved this by putting a default onError handler on the query object (a react-query object) that I use for all the API calls.)

I built upon the Request class from this SO answer to refresh the token and handle the refresh failures.
Now my Request looks like this:
import axios from "axios";
import {getLocalStorageToken, logOut, refreshToken} from "./authentication";
class Request {
ADD_AUTH_CONFIG_HEADER = 'addAuth'
constructor() {
this.baseURL = process.env.REACT_APP_USER_ROUTE;
this.isRefreshing = false;
this.failedRequests = [];
this.axios = axios.create({
baseURL: process.env.REACT_APP_USER_ROUTE,
headers: {
clientSecret: this.clientSecret,
},
});
this.beforeRequest = this.beforeRequest.bind(this);
this.onRequestFailure = this.onRequestFailure.bind(this);
this.processQueue = this.processQueue.bind(this);
this.axios.interceptors.request.use(this.beforeRequest);//<- Intercepting request to add token
this.axios.interceptors.response.use(this.onRequestSuccess,
this.onRequestFailure);// <- Intercepting 401 failures
}
beforeRequest(request) {
if (request.headers[this.ADD_AUTH_CONFIG_HEADER] === true) {
delete request.headers[this.ADD_AUTH_CONFIG_HEADER];
const token = getLocalStorageToken();//<- replace getLocalStorageToken with your own way to retrieve your current token
request.headers.Authorization = `Bearer ${token}`;
}
return request;
}
onRequestSuccess(response) {
return response.data;
}
async onRequestFailure(err) {
console.error('Request failed', err)
const {response} = err;
const originalRequest = err.config;
if (response.status === 401 && err && originalRequest && !originalRequest.__isRetryRequest) {
if (this.isRefreshing) {
try {
const token = await new Promise((resolve, reject) => {//<- Queuing new request while token is refreshing and waiting until they get resolved
this.failedRequests.push({resolve, reject});
});
originalRequest.headers.Authorization = `Bearer ${token}`;
return this.axios(originalRequest);
} catch (e) {
return e;
}
}
this.isRefreshing = true;
originalRequest.__isRetryRequest = true;
console.log('Retrying request')
console.log('Previous token', getLocalStorageToken())
try {
const newToken = await refreshToken()//<- replace refreshToken with your own method to get a new token (async)
console.log('New token', newToken)
originalRequest.headers.Authorization = `Bearer ${newToken}`;
this.isRefreshing = false;
this.processQueue(null, newToken);
return this.axios(originalRequest)
} catch (err) {
console.error('Error refreshing the token, logging out', err);
await logOut();//<- your logout function (clean token)
this.processQueue(err, null);
throw response;//<- return the response to check on component layer whether response.status === 401 and push history to log in screen
}
}
throw response;
}
processQueue(error, token = null) {
this.failedRequests.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
this.failedRequests = [];
}
}
const request = new Request();
export default request;

My problem is, when the refresh token expires, I am not able to catch
the error and redirect the user to re-authenticate. Nothing I've tried
catches the error. The (current) code for the auto-refresh hook is:
What is the return code from your api if the access token expired ?
if it is different than 401 (default) you need to configure, see exanoke 403:
createAuthRefreshInterceptor(axios, refreshAuthLogic, {
statusCodes: [ 401, 403 ] // default: [ 401 ]
});

Related

Getting error on console despite handling it (MERN + Redux)

I am sending axios get request whose end-point sends the user associated with the token stored in localStorage and then the redux state is updated with the user. When I don't have a token the end-point return a res with status 401 with message "Unauthorized" and then I handle it in the catch statement and set the "error" redux state. But even after doing this the error is displayed on the console like this:
Failed to load resource: the server responded with a status of 401 (Unauthorized) /users/auth:1
This is the function which makes api call and authorizes the user:
export function loadUser(){
return function (dispatch,getState){
dispatch(userLoading());
const token = getState().auth.token;
const config = {
headers:{
'Content-Type':'application/json'
}
}
if(token) config.headers['auth-token']=token;
axios.get('http://localhost:80/users/auth',config)
.then(user => {
dispatch(clearError())
dispatch(userLoaded(user.data))
})
.catch(error => {
dispatch(setError(error.response.status,error.response.data.msg));
dispatch(authError());
})
}
}
This is the middleware which handles the token before hitting the endpoint (In my case response is returned from here itself since there is no token sent):
function auth(req,res,next){
const token = req.header('auth-token');
if(!token) res.status(401).json({msg:"Unauthorized"})
else{
try{
const decoded = jwt.verify(token,jwt_secret);
req.user = decoded;
next();
}
catch(e){
res.status(400).json({msg:"Invalid token"})
}
}
}
I'm not able to figure out why am I getting error on console (State is getting updated as desired)
It is actually impossible to do with JavaScript. because of security concerns and a potential for a script to hide its activity from the user.
The best you can do is clearing them from your console.
console.clear();
I think it is because you are not getting the token when consulting your API.
If this is the case I recommend you use defaults.headers.common in this way
const axiosApi = axios.create({ baseURL: "http://localhost:80" });
const headerAuth = () => {
const token = getMyToken();
if (token) {
axiosApi.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else {
delete axiosApi.defaults.headers.common.Authorization;
}
};
export function loadUser(){
headerAuth(); // <-----
return function (dispatch,getState){
dispatch(userLoading());
axiosApi.get('/users/auth',config)
.then(user => {
dispatch(clearError())
dispatch(userLoaded(user.data))
})
.catch(error => {
dispatch(setError(error.response.status,error.response.data.msg));
dispatch(authError());
})
}
I recommend that you do not store the token in the REDUX but in sessionStorage

Expo AuthSession promptAsync - no way to wait for return when getting a new access token

We have a managed Expo app using AuthSession specifically to deploy our app to a web environment. Authenticating using Auth0 and a PKCE grant.
Have run into an issue trying to get a new access token when our current access token has expired. Before making an HTTP request we check to see if our access token has expired and if it has we use AuthSession.promptAysnc() to get a new one. The problem is we have no way of waiting for promptAsync to finish before making our HTTP request.
Have we missed something?
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
redirectUri,
clientId,
scopes,
prompt: AuthSession.Prompt.Login,
extraParams: {
audience
}
},
{ authorizationEndpoint, tokenEndpoint, revocationEndpoint }
);
const refreshAuthSessionAsync = async () => {
...
promptAsync({ useProxy });
// sleep hack - wait until we have something to return
await new Promise(r => setTimeout(r, 1000));
return authState;
};
const handleFetchAsync = async (url, request) => {
...
if (Date.parse(expiresAt) < (new Date()).getTime()) {
newAuthState = await refreshAuthSessionAsync();
return newAuthState;
}
...
}
I'm working through a similar problem with an Expo managed app trying to access a refresh token after authenticating through Auth0 using a PKCE flow.
I came across this repo
https://github.com/tiagob/create-full-stack/blob/master/packages/cfs-expo-auth0/src/index.tsx
while reading through the comments of this Expo issue about refresh tokens and Auth0
https://github.com/expo/examples/issues/209.
I haven't implemented anything from this repo yet, but it seems like some of its approach may solve the issue I'm having and hopefully your problem.
I used a useEffect hook to "wait" for the request to finish and a useState hook to save the current state.
useEffect(() => {
if (state !== stateMode.request_running) return;
if (request === null || response === null || response.type !== "success") {
// Request not done
}
else {
// Request done
onLogin();
setState(stateMode.request_done);
}
});
The state I used
const stateMode = {
initialized: 1,
request_running: 2,
request_done: 3
};
const [state, setState] = useState(stateMode.initialized);
And before calling promptAsync I set
setState(stateMode.request_running);

Angular 10 : ngOnInit two promises

I'm currently trying to implement in my Angular app the connection to Strava API.
To resume quickly:
User clicks on a button to connect to Strava
It is redirected to Strava for authentication(using PKCE)
Strava redirects to my app with a code
In the ngoninit I'm checking for route params and if I have the code, I launch two promises chained: the first one to get the Access Token from Strava then the recording into a DB(Firebase).
The problem is that sometimes the data is recorded in firebase and sometimes it is not. The behavior is not systematic. Strange thing is that I go into my postNewToken everytime because the console logs it.
If I just record to firebase (without strava token request) in ngOnInit(), it is created in 100% of the cases.
If I have a button that launches the token request and record into firebase, it seems to work everytime.
I have no idea how to solve it. It seems more a question of chaining promises into ngOnInit but I have no idea even how to bypass it.
The code from my component:
ngOnInit() {
const stravaCode = this.route.snapshot.queryParamMap.get('code');
if (stravaCode !== undefined) {
this.stravaService.handleStravaAuthorizationCode(stravaCode);
}
And in the service associated:
// Handle Strava Code received
handleStravaAuthorizationCode(authorizationCode: string) {
this.getStravaToken(authorizationCode).then(res => {
this.postNewToken(res).then(res => {
this.router.navigate(['encoding']);
});
});
}
// Get Strava access token to make the requests to the API -> only done once
getStravaToken(authorizationCode: string){
if (authorizationCode !== undefined){
console.log('Authorization code: ' + authorizationCode);
const data = {
client_id: environment.strava.client_id,
client_secret: environment.strava.client_secret,
code: authorizationCode,
grant_type: 'authorization_code'
};
return this.http.post<StravaToken>(this.stravaTokenURL, data)
.pipe(catchError(this.errorService.handleHttpError)).toPromise();
}
}
postNewToken(stravaToken: StravaToken) {
if (this.authService.isLoggedIn) {
console.log('Recording strava token into Firebase');
console.log(stravaToken);
return this.afs.collection('strava_tokens')
.add(stravaToken).then(res => console.log(res), err => console.log(err));
} else {
return Promise.reject(new Error('No User Logged In!'));
}
}
Finally, I understood.
I simply was not waiting for the connection to firebase to be established. So, I could not post any new data because I was not authenticated.

Axios interceptor issue in production

I am experiencing an issue when developing an application and attempting to utilize a refresh token. I am using ADFS for authentication, where I get an id_token that expires every hour and a refresh token that lasts 8 hours.
In development, the below script works perfectly as intended and reaches out to the server for a refresh.
In production, it gets new tokens, but it never retries the original request. I am trying to find out why it is different on webpack-dev-server vs production.
Any help would be much appreciated!
P.S. Using Babel Presets: babel-preset-env and babel-preset-stage-2
axios.js
import axios from 'axios'
// Set baseURL for development and production
const baseURL = process.env.NODE_ENV === 'development' ? '//localhost:3001/api' : '/api'
// Create instance of axios with correct baseURL
const instance = axios.create({
baseURL
})
// Intercept responses
instance.interceptors.response.use((response) => {
return response
}, async (error) => {
// Pull config, status and data from the error
const { config, response: { status, data } } = error
// Pull tokens from local storage
let currentTokens = JSON.parse(localStorage.getItem('tokens')) || null
// If response errors at 401, token is still valid and we have tokens in localStorage
if(status === 401 && data.token_invalid === undefined && currentTokens && !config._retry) {
config._retry = true
try {
// Ask server for new token
const authenticate = await instance.post('/user/login', {refresh_token: currentTokens.refresh_token})
// Pull tokens and success from authenticated request
const { tokens, success } = authenticate.data
// If successful, set access_token, id_token, headers and localStorage
if(success) {
currentTokens.access_token = tokens.access_token
currentTokens.id_token = tokens.id_token
const bearer = `Bearer ${tokens.id_token}`
config.headers['Authorization'] = bearer
Object.assign(instance.defaults, {headers: {Authorization: bearer}})
localStorage.setItem('tokens', JSON.stringify(currentTokens))
// Rerun original request
return instance(config)
}
} catch (e) {
// Catch any errors
console.log(e)
return
}
} else if(data && data.token_invalid !== undefined && data.token_invalid) {
// If refresh has expired, take user to ADFS to reauthenticate
location = `${process.env.OAUTH_CLIENT_EP}?client_id=${process.env.AZURE_CLIENT_ID}&redirect_uri=${process.env.REDIRECT_URI}&resource=${process.env.REDIRECT_URI}&response_type=code`
return
} else {
// Console log all remaining errors
return
}
})
export default instance
Found the issue. It appears that since I'm using both relative and absolute urls for the baseURL, the absolute URL in development is being processed correctly, however the relative URL is being chained to the original request.
In other words, sending in production, the url looks like: /api/api/actual/request, where it should just be /api/actual/request.
I solved this by adding a API_URL to my config files, and input the absolute url for both development and production and the updated my instance creation to the following.
const instance = axios.create({
baseURL: process.env.API_URL
})
Thanks to all who viewed and attempted to help. Have a great day everyone!

Wrap javascript fetch to add custom functionality

I would like to know if it is possible to do this, because I'm not sure if I'm wrong or if it isn't possible. Basically, what I want to do is to create a wrap function for native fetch javascript function. This wrap function would implement token validation process, requesting a new accessToken if the one given is expired and requesting again the desired resource. This is what I've reached until now:
customFetch.js
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}
consumer.js
const getResourcePrivate = () => {
const url = MAIN_URL + '/resource';
customFetch(url, {
method: 'get'
},{
url: AUTH_SERVER_TOKEN,
unauthorizedRedirect: AUTH_URI,
tokenName: TOKEN_NAME
}).then((json) => {
const resource = json ? json.resource : null;
if (resource) {
console.log(resource);
}
else {
console.log('No resource has been provided.');
}
});
}
I'll try to explain a little better the above code: I want to make transparent for users the token validation, in order to let them just worry about to request the resource they want. This approach is working fine when the accessToken is still valid, because the return request instruction is giving to the consumer the promise of the fetch request.
Of course, when the accessToken has expired and we request a new one to auth server, this is not working. The token is refreshed and the private resource is requested, but the consumer.js doesn't see it.
For this last scenario, is it possible to modify the flow of the program, in order to refresh the accessToken and perform the server call to get the private resource again? The consumer shouldn't realize about this process; in both cases (accessToken is valid and accessToken has expired and has been refreshed) the consumer.js should get the private requested resource in its then function.
Well, finally I've reached a solution. I've tried to resolve it using a Promise and it has work. Here is the approach for customFetch.js file:
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
const requestResource = (resolve) => {
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken(resolve);
}
else {
resolve(d.json());
}
});
}
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = (resolve) => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
if (d.status === 401) {
// Unauthorized
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
requestResource(resolve);
}
else {
console.log('no token has been sent');
return null;
}
});
}
let promise = new Promise((resolve, reject) => {
requestResource(resolve);
});
return promise;
}
Basically, I've created a Promise and I've called inside it to the function which calls to server to get the resource. I've modified a little the request(now called requestResource) and refreshAccessToken in order to make them parametrizable functions. And I've passed to them the resolve function in order to "resolve" any function once I've received the new token.
Probably the solution can be improved and optimized, but as first approach, it is working as I expected, so I think it's a valid solution.
EDIT: As #Dennis has suggested me, I made a mistake in my initial approach. I just had to return the promise inside the refreshAccessToken function, and it would worked fine. This is how the customFetch.js file should look (which is more similar to the code I first posted. In fact, I've just added a return instruction inside the function, although removing the start and end brackets would work too):
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
return window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}

Categories