Search code examples
node.jsexpressjwtreact-reduxhttp-token-authentication

Triggering a large file download from a fully-formed response


The Problem

I have a Node.js end-point that properly triggers an arbitrarily-large file download when accessed using the following:

response.setHeader('Content-disposition', 'attachment; filename=' + fileName);
response.set('Content-Type', 'text/csv');
response.status(200);
result.pipe(response);

where result is a transform stream, and response is an Express object.

This works fine when directly accessing the end-point in Chrome, Firefox, Internet Explorer, etc. The problem arises from trying to hit the end-point when token-based authentication is enabled.

From a user's perspective, when they click on a button, a file is downloaded.

How do I make the button hit this end-point with the correct authentication token in the request's header and cause a file to be downloaded?

Brainstorm of Some Possible Approaches

  1. When the user clicks on the button, it fires an action that's handled by the redux-api-middleware, which makes the GET request to the end-point (with the authentication token automatically included in the request). This middleware saves the response in a variable, which is picked up by a React component. In this React component, if the browser being used (i.e. Chrome and Opera) supports streams, response.body will exist so you can do something like the following.

    if (response.body) {
        const csvReader = response.body.getReader();
        const textDecoder = new TextDecoder();
        const processCsvRow = (csvRow) => {
            if (csvRow.done) {
                console.log('Finished downloading file.');
                return Promise.resolve();
            }
    
            // Write to new file for user to download
            console.log(textDecoder.decode(csvRow.value, {stream: true}));
    
            return csvReader.read().then(processCsvRow);
        };
    
        csvReader.read().then(processCsvRow);
    }
    

    With this approach, I'd need to investigate how to handle the data if it's being received by a non-stream-supporting browser.

  2. When the user clicks on the button, it fires an action that saves the end-point into the Redux store by the reducer, which triggers a React component to create an anchor tag that is automatically clicked.

    const link = document.createElement('a');
    document.body.appendChild(link); // Firefox requires the link to be in the body
    link.href = endPoint;
    link.target = '_blank';
    link.click();
    document.body.removeChild(link); // Remove the link when done
    

    where endPoint is the end-point that responds with the file.

    This approach works when authentication is disabled. When authentication is re-enabled, the authentication token must somehow be injected into the anchor tag's request header.

  3. Similar to #2, look into simulating an anchor-click by constructing an HTTP request with the authentication token built in.
  4. Combining elements from #1 and #2, when the user clicks on the button, an action is fired that sends a GET request to the end-point, which returns a second temporary unsecured end-point that returns the actual file response. Pass this temporary unsecured end-point to the anchor tag in #2.
  5. Redirect the response to a new tab/window somehow.
  6. Pass the authentication token as a URL parameter (this is a security concern), then use #2.
  7. Create a new end-point that generates and returns a new temporary one-time-use download-only token. Use this new end-point to get a temporary token to pass as a URL parameter into the original end-point via #2.

Solution

  • I've decided to go with Approach #7 for now.