Search code examples
node.jsexpressasync-awaitlocking

Preventing Multiple Concurrent Requests in Node.js with Async/Await and some sort of Lock


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

Solution

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