Give me my stuff! Authenticated File Download in JavaScript


Tue 30 June 2015 By Josh Fletcher

Not long ago, I presented a webinar where I demonstrated a standalone Web application that used CMIS to access content from a Nuxeo repository. One particular feature I was excited to show was the ability to download a rendition of a document living in the Nuxeo Platform. Much to my embarrassment I discovered later that this example did not work at all unless I was already logged in to the Nuxeo application. Kind of defeats the purpose of building a standalone web application!

So I set about rectifying this oversight. It turned out to be a difficult problem to research, but with a pretty simple solution. Since the issue is not really Nuxeo-specific, I decided to handle it in a separate blog. This information should benefit anyone needing to download files that require authentication from JavaScript code.

Note: I will be blogging about my CMIS client in a future post as well.

User Story


I want to be able to download the PDF rendition of a Nuxeo document via CMIS using a single-page Web application. To be clear the Web application is not a Nuxeo application, just a basic app running on Apache, for example. The request involves a simple URL like this:

http://localhost:8080/nuxeo/json/cmis/default/root?succinct=true&streamId=nuxeo%3Arendition%3Apdf&cmisselector=content&objectId=07cdb579-b845-46a7-b22f-f49fa4f7de8b&download=attachment

The Problem

Authentication


Of course the request must be authenticated. In general, I want to avoid use of the browser's ugly authentication dialog, so this means the authentication needs to occur in the code. Thus a simple <a> tag will not suffice. If the user clicks a link, this navigates away from the single-page application, thus any authentication information is lost and the user gets the browser's authentication dialog. The same problem applies to techniques that involve window.open, using a <form>, using an <iframe>, etc.

Save the File Locally


In addition, I want the user experience to be just like any other file download. Maybe it goes to the "Downloads" folder, or even opens in the browser, depending on the file type and browser support.

The Solution


The solution is made of two main parts:


  • An XMLHTTPRequest (XHR) call to retrieve the file (as a blob); the call is authenticated.

  • A JavaScript library that takes the blob returned by XHR and saves it on the local system as a file.


Authenticated File Download

The Explanation


To get the blob you just need to authenticate via XHR. The built-in params (username and password) for XHR don't seem to work (in fact they may not be intended to be used in this way). However, manually adding the Authorization header DOES work. For example:

xhr.setRequestHeader("Authorization", "Basic " + Base64.encode(userName + ":" + password));

To save the blob I used FileSaver.js. As far as I can decipher, here is what it does:


  • Save the blob to temporary storage using the JavaScript FileAPI.

  • Create an <a> element and assign a blob URL to it that references the above file.

  • Enable the HTML5 download attribute for this element.


    • Note: This is how it works for Chrome and Firefox, but it depends on the browser. For example Safari does not yet support download. FileSaver.js is robust enough to support several browsers.



  • "Click" the element via JavaScript.


Note that FileSaver.js implements the HTML5 W3C saveAs() interface. At some point it's likely the browsers will implement saveAs() natively.

The Context


The solution is deceptively simple. It took me several days of research, understanding, and testing to find the answer. While scouring the internet I found there are several ways to "download a file" using JavaScript and several ways to make authenticated requests using JavaScript, but I found literally nothing that combines the two on the client side. Every solution I found that attempted to combine the two involved modifications on the server side.

Caveat: the solution, in particular the file download, relies on the FileAPI from HTML5, as well as the download attribute. The point is it works well mainly in modern browsers.

The Tips


  • You need to configure CORS in Nuxeo to allow CMIS requests from a Web app. I chose to restrict it to just the JSON binding from localhost. Here is my contribution:

<extension target="org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerService" point="corsConfig">
<corsConfig name="forCMISBrowserBinding" allowOrigin="http://localhost"&gt;
<pattern>/nuxeo/json/cmis.*</pattern>
</corsConfig>
</extension>


  • download.js appears to be an older (but viable) solution. The reason I mention it is because it appears to skip the step of saving the file to local storage. It takes the blob returned by XHR and creates an anchor tag that points to it directly in memory (still using the blob URL syntax). But the behavior in Safari is far inferior to FileSaver.js.


  • You can't access the filename using XHR once you're using CORS. There's a great explanation here. The summary is that you are limited to "simple response headers" when using XHR with CORS. Nuxeo returns the filename in the content-disposition header; this one is not accessible. It's no big deal when using CMIS because you can just get the document name via the CMIS object.


  • Incidentally XHRs withCredentials does nothing and appears to have nothing to do with the username/password passed to XHR. It's used for passing cookies with CORS requests. CORS normally restricts/does not pass cookies across domains.

The Example


I tried to make the example as simple as possible to focus on a) authenticating the request and b) saving the file. By no means is it meant to be a "best practice" example, just a simple explanation of how it works.

Check out the example application here!


Category: Product & Development
Tagged: CMIS, How to, JavaScript