Axios interceptor issue in production - javascript

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!

Related

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

(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 ]
});

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);

Next.js getInitialProps cookies

I have encountered an issue regarding fetching data from the getInitialProps function in Next.js
The scenario is this: when a user first visits a page, I make an HTTP request to a distant API which returns me data that I need for the application. I make the request inside the getInitialProps method because I want the content to be fully rendered when I ship the content to the user.
The problem is, when I make this request, the API returns me a session cookie which I need to store inside the browser, not the server that is rendering the content. This cookie will have to be present inside future client-side requests to the API. Otherwise, the API returns me 403.
My question is: If I'm performing this request from the server, and because of that the response also comes back to the server, How can I set the cookie for the browser so that I could make client-side requests to the API?
I tried manipulating the domain option of the cookie but I cannot set another domain. The browser just ignores it.
Here is how my getInitialProps looks like:
static async getInitialProps(appContext) {
const { Component, ctx, router } = appContext;
const { store } = ctx;
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(appContext);
}
const { hotelId, reservationId } = router.query;
if (!hotelId || !reservationId) return { pageProps };
// Fetching reservation and deal data
try {
const { data, errors, session } = await fetchData(hotelId, reservationId);
if (data) {
store.dispatch(storeData(data));
}
// This works, but the domain will be the frontend server, not the API that I connecting to the fetch the data
if (session) {
ctx.res.setHeader('Set-Cookie', session);
}
// This doesn't work
if (session) {
const manipulatedCookie = session + '; Domain: http://exampe-api.io'
ctx.res.setHeader('Set-Cookie', manipulatedCookie);
}
if (errors && errors.length) {
store.dispatch(fetchError(errors));
return { errors };
} else {
store.dispatch(clearErrors());
return {
...pageProps,
...data
};
}
} catch (err) {
store.dispatch(fetchError(err));
return { errors: [err] };
}
return { pageProps };
}
The fetchData function is just a function which sends a request to the API. From the response object, I'm extracting the cookie and then assign it to the session variable.
getInitialProps is executed on the client and server. So when you write your fetching function you have fetch conditionally. Because if you make request on the server-side you have to put absolute url but if you are on the browser you use relative path. another thing that you have to be aware, when you make a request you have to attach the cookie automatically.
in your example you are trying to make the request from _app.js. Next.js uses the App component to initialize the pages. So if you want to show some secret data on the page, do it on that page. _app.js is wrapper for all other components, anything that you return from getInitialProps function of _app.js will be available to all other components in your application. But if you want to display some secret data on a component upon authorization, i think it is better to let that component to fetch the data. Imagine a user logins his account, you have to fetch the data only when user logged in, so other endpoints that does not need authentication will not access to that secret data.
So let's say a user logged in and you want to fetch his secret data. imagine you have page /secret so inside that component I can write like this:
Secret.getInitialProps = async (ctx) => {
const another = await getSecretData(ctx.req);
return { superValue: another };
};
getSecretData() is where we should be fetching our secret data. fetching actions are usually stored in /actions/index.js directory. Now we go here and write our fetching function:
// Since you did not mention which libraries you used, i use `axios` and `js-cookie`. they both are very popular and have easy api.
import axios from "axios";
import Cookies from "js-cookie";
//this function is usually stored in /helpers/utils.js
// cookies are attached to req.header.cookie
// you can console.log(req.header.cookie) to see the cookies
// cookieKey is a param, we pass jwt when we execute this function
const getCookieFromReq = (req, cookieKey) => {
const cookie = req.headers.cookie
.split(";")
.find((c) => c.trim().startsWith(`${cookieKey}=`));
if (!cookie) return undefined;
return cookie.split("=")[1];
};
//anytime we make request we have to attach our jwt
//if we are on the server, that means we get a **req** object and we execute above function.
// if we do not have req, that means we are on browser, and we retrieve the cookies from browser by the help of our 'js-cookie' library.
const setAuthHeader = (req) => {
const token = req ? getCookieFromReq(req, "jwt") : Cookies.getJSON("jwt");
if (token) {
return {
headers: { authorization: `Bearer ${token}` },
};
}
return undefined;
};
//this is where we fetch our data.
//if we are on server we use absolute path and if not we use relative
export const getSecretData = async (req) => {
const url = req ? "http://localhost:3000/api/v1/secret" : "/api/v1/secret";
return await axios.get(url, setAuthHeader(req)).then((res) => res.data);
};
this is how you should implement fetching data in next.js

How to integrate Dialogflow v2 with javascript frontend (Vue.js)

I'm trying to integrate Dialogflow with Vue.js (and axios) according to the documentation's sample HTTP request: https://dialogflow.com/docs/reference/v2-auth-setup and detectIntent: https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/projects.agent.sessions/detectIntent.
I have a service account set up with sufficient permissions, and given it the path parameters and request body as shown in the documentation, but I keep getting 'Error: Request failed with status code 400' when calling the detectIntent API.
There are a few things I'm not sure of, though:
How do I get a sessionId? Currently I just copy the sessionId from Firebase Function logs which shows up when entering a query through the Dialogflow console directly.
How do I actually implement $(gcloud auth print-access-token) in javascript code? Currently I'm running the command in the terminal and pasting the token in the code, just to test if the API works, but I have no clue how it should be implemented.
(Perhaps useful, I have fulfillment set up in a functions folder, and that is working nicely.)
Thanks in advance!
<script>
import axios from 'axios'
export default {
name: 'myChatBot',
mounted () {
// Authorization: Bearer $(gcloud auth print-access-token)
const session = 'projects/mychatbot/agent/sessions/some-session-id'
const token = 'xxxxxxxxxxxx'
axios.defaults.baseURL = 'https://dialogflow.googleapis.com'
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios
.post(`/v2beta1/${session}:detectIntent`, {
"queryInput": {
"text": "add buy milk to inbox",
"languageCode": "en-US"
}
})
.then(response => console.log(response))
.catch(error => console.log(error))
}
}
</script>
You can use JWT authorization to handle your #2 question. You just need to put your JSON file someplace safe. https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth
The reason you are getting the code 400 is because your params are a little off. Here is how your post should look (I've also added some extra code to handle token generation):
<script>
import axios from 'axios'
import { KJUR } from 'jsrsasign'
const creds = require('./YOUR_JSON_FILE')
export default {
name: 'myChatBot',
data() {
return {
token: undefined,
tokenInterval: undefined
}
},
created() {
// update the tokens every hour
this.tokenInterval = setInterval(this.generateToken, 3600000)
this.generateToken()
},
mounted () {
this.detectIntent('add buy milk to inbox')
},
beforeDestroy() {
clearInterval(this.tokenInterval)
},
methods: {
generateToken() {
// Header
const header = {
alg: 'RS256',
typ: 'JWT',
kid: creds.private_key_id
}
// Payload
const payload = {
iss: creds.client_email,
sub: creds.client_email,
iat: KJUR.jws.IntDate.get('now'),
exp: KJUR.jws.IntDate.get('now + 1hour'),
aud: 'https://dialogflow.googleapis.com/google.cloud.dialogflow.v2.Sessions'
}
const stringHeader = JSON.stringify(header)
const stringPayload = JSON.stringify(payload)
this.token = KJUR.jws.JWS.sign('RS256', stringHeader, stringPayload, creds.private_key)
},
detectIntent(text, languageCode = 'en-US') {
if (!this.token) {
// try again
setTimeout(this.detectIntent, 300, text, languageCode)
return
}
// error check for no text, etc.
const session = 'projects/mychatbot/agent/sessions/some-session-id'
axios.defaults.baseURL = 'https://dialogflow.googleapis.com'
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios
.post(`/v2beta1/${session}:detectIntent`, {
queryInput: {
text: {
text,
languageCode
}
}
})
.then(response => console.log(response))
.catch(error => console.log(error))
}
}
}
</script>
You can see that in QueryInput it's taking 1 of 3 different types of objects ("text" being one of those).
In the link, it's stated under the HTTP request session path parameters that "It's up to the API caller to choose an appropriate session id.
It can be a random number or some type of user identifier (preferably hashed).
For integration with Dialogflow V2, here's an example for doing with third-party tools that are easy to integrate and start using.
The sessionId is an identifier you can provide that will indicate to Dialogflow whether subsequent requests belong to the same "session" of user interaction (see docs).
For a client's first request to the API, you could just generate a random number to use as a session ID. For subsequent requests from the same client (e.g. if a user is continuing to converse with your agent) you can reuse the same number.
Your implementation of the token management looks fine, as long as the service account you are using has appropriately limited access (since this token could potentially allow anyone to make requests to Google Cloud APIs). For additional security, you could consider proxying the request to Dialogflow through your own server rather than making the call from the client.

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