Improving Authentication Lock to Prevent Multiple Concurrent Requests
I have a Node.js server that acts as a reverse proxy to my partner APIs. My mobile app makes 3 concurrent requests to my Node server, which then forwards the requests to the appropriate partner API.
The partner API requires an access_token via an authentication request. The token expires every hour, so once it becomes invalid, my server needs to re-authenticate.
The issue is that when the token expires or is invalid or null, all 3 concurrent requests see the token as invalid and try to authenticate simultaneously. However I want to just re authenticate 1 time.
I want to ensure that: Only one authentication request is made. The other two requests wait for the first authentication request to complete and then proceed with the newly acquired token.
Right now when I hit this 3 times concurrently with my iOS app, all 3 requests authenticate. I want to implement a better lock to my authentication. any ideas?
// api.js
let access_token = null;
let tokenExpiration = null;
let refreshingToken = false;
function isAccessTokenValid() {
return Date.now() < tokenExpiration;
}
async function authenticate() {
if (refreshingToken) {
console.log("Token refresh in progress, waiting...");
while (refreshingToken) {
// Wait 1s before checking again
await new Promise((resolve) => setTimeout(resolve, 100));
}
return access_token;
}
refreshingToken = true;
let token = null;
try {
token = getBearerToken();
} catch(error) {
console.log('token err' + error);
} finally {
refreshingToken = false;
}
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Basic ${token}`
};
let url = new URL('https://myapi.com/auth');
let body = new URLSearchParams();
body.append("grant_type", "client_credentials");
try {
let res = await axios.post(url.toString(), body, { headers });
access_token = res.data.access_token;
tokenExpiration = Date.now() + (res.data.expires_in * 1000);
console.log('Finished setting token');
return
} catch(error) {
throw error
}
}
const energyUsage = async (req, res, next) => {
// check if access_token != null
if (access_token == null || !isAccessTokenValid()) {
await authenticate();
}
// make request
const url = new URL('https://myapi.com');
let params = new URLSearchParams();
const headers = {
"Authorization": `Bearer ${access_token}`
}
try {
const apiResponse = await axios.get(decodeURIComponent(url.toString()), {headers});
const data = handleResponse(apiResponse.data);
res.status(apiResponse.status).json(data);
} catch(error) {
return next(new ErrorHandler('error fetching data', 400));
}
}
You can use this workaround.
let access_token = null;
let tokenExpiration = null;
let refreshingToken = false;
let isProcessing = null; // New global check
const energyUsage = async (req, res, next) => {
// check if access_token != null
if ((access_token == null || !isAccessTokenValid()) && !isProcessing ) {
isProcessing = authenticate();
}
await isisProcessing; // This will await next request until it's fulfilled
// make request
const url = new URL('https://myapi.com');
let params = new URLSearchParams();
const headers = {
"Authorization": `Bearer ${access_token}`
}
try {
const apiResponse = await axios.get(decodeURIComponent(url.toString()), {headers});
const data = handleResponse(apiResponse.data);
res.status(apiResponse.status).json(data);
} catch(error) {
return next(new ErrorHandler('error fetching data', 400));
}
}