TL;DR
How to download/save a file using Angular and JWT authentication without leaving a token trail in the browser?
My Angular/Node app is secured over HTTPS and uses JWT for authentication. The JWT is stored in sessionStorage and passed in the Authorization header field for all AJAX requests to the server.
I need functionality in the app to download a file so that it's automatically saved by the browser (or a popup displayed where to save etc.).
It should work ideally in any browser that can run Angular.
I have looked at the following:
AJAX requests. This doesn't work because of inherent security measures preventing a browser from saving a file locally.
Pass the JWT in a Cookie - cookies are something I want to avoid using, hence the reason for using sessionStorage.
Pass the JWT in a query string but this means it will be logged in the server logs, and more importantly can be seen in browser history.
iframe that contains a form that POSTS the data. Can't set a header with this method.
Any other options?
The iframe method was close. Just need to set the server up to accept the JWT from the body of a POST rather than the query string.
It's not the most elegant solution having to use an iframe, but it seems to meet the requirements. Here's the directive I used:
.directive('fileDownload', function () {
return {
restrict: 'A',
replace: false,
template: "<iframe style='position:fixed;display:none;top:-1px;left:-1px;' />",
link: function (scope, element) {
element.click(function() {
var iframe = element.find('iframe'),
iframeBody = $(iframe[0].contentWindow.document.body),
form = angular.element("<form method='POST' action='/my/endpoint/getFile'><input type='hidden' name='foo' value='bar' /><input type='hidden' name='access_token' value='" + scope.access_token + "' /></form>");
iframeBody.append(form);
form.submit();
});
}
}
});
And in express middleware pop the JWT into the header before validating it with the express-jwt module
if (req.body && req.body.hasOwnProperty('access_token')) {
req.headers.authorization = 'Bearer ' + req.body.access_token;
}
After scouring the web for solutions and not finding anything satisfactory, I finally arrived at a solution that uses bits and pieces of advice from all over.
Hope this helps someone looking for a secure solution.
I used the iframe solution as described by Bagofjuice, but using the jquery.filedownload plugin (see https://github.com/johnculviner/jquery.fileDownload).
This is what I did to secure this request:
When a user clicks the download button or link, I request a new token from the server (created a dedicated server API call for this) that I append as a query string to the file download url and then use the filedownload plugin to download the file. Note that this token has a very short expiration time since it is used immediately to download the file and should not be used more than once.
Then on the server side, when a token comes via a query parameter, I serve the response and expire the token immediately (in addition to the expiration time to eliminate even the smallest of windows for exploitation).
Technics for instant expiration can vary. The one I used is that when I create this temporary token, I store it in an in-memory cache and upon first usage, I remove it from the cache. If it does not exist in the cache when I receive it via a request, I consider it an invalid token. This prevents the token from being used more than once via a query parameter. Thus, it does not matter if this is logged in server side logs or leaves a trail on the browser. For web farms, you can easily store this in a distributed cache.