To reiterate, whatever you do, don't store a JWT in local storage (or session storage). If any of the third-party scripts you include in your page is compromised, it can access all your users' tokens. To keep them secure, you should always store JWTs inside an httpOnly cookie.
JSON Web Token (JWT, pronounced /dʒɒt/, same as the word "jot") is a proposed Internet standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims. The tokens are signed either using a private secret or a public/private key.
Storing JWT Token We can store it as a client-side cookie or in a localStorage or sessionStorage. There are pros and cons in each option but for this app, we'll store it in sessionStorage.
Here's a way to download it on the client using the download attribute, the fetch API, and URL.createObjectURL. You would fetch the file using your JWT, convert the payload into a blob, put the blob into an objectURL, set the source of an anchor tag to that objectURL, and click that objectURL in javascript.
let anchor = document.createElement("a");
document.body.appendChild(anchor);
let file = 'https://www.example.com/some-file.pdf';
let headers = new Headers();
headers.append('Authorization', 'Bearer MY-TOKEN');
fetch(file, { headers })
.then(response => response.blob())
.then(blobby => {
let objectUrl = window.URL.createObjectURL(blobby);
anchor.href = objectUrl;
anchor.download = 'some-file.pdf';
anchor.click();
window.URL.revokeObjectURL(objectUrl);
});
The value of the download
attribute will be the eventual file name. If desired, you can mine an intended filename out of the content disposition response header as described in other answers.
Based on this advice of Matias Woloski from Auth0, known JWT evangelist, I solved it by generating a signed request with Hawk.
Quoting Woloski:
The way you solve this is by generating a signed request like AWS does, for example.
Here you have an example of this technique, used for activation links.
I created an API to sign my download urls:
Request:
POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}
Response:
{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}
With a signed URL, we can get the file
Request:
GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c
Response:
Content-Type: multipart/mixed; charset="UTF-8"
Content-Disposition': attachment; filename=protected.file
{BLOB}
This way you can do it all on a single user click:
function clickedOnDownloadButton() {
postToSignWithAuthorizationHeader({
url: 'https://path.to/protected.file'
}).then(function(signed) {
window.location = signed.url;
});
}
An alternative to the existing "fetch/createObjectURL" and "download-token" approaches already mentioned is a standard Form POST that targets a new window. Once the browser reads the attachment header on the server response, it will close the new tab and begin the download. This same approach also happens to work nicely for displaying a resource like a PDF in a new tab.
This has better support for older browsers and avoids having to manage a new type of token. This will also have better long-term support than basic auth on the URL, since support for username/password on the url is being removed by browsers.
On the client-side we use target="_blank"
to avoid navigation even in failure cases, which is particularly important for SPAs (single page apps).
The major caveat is that the server-side JWT validation has to get the token from the POST data and not from the header. If your framework manages access to route handlers automatically using the Authentication header, you may need to mark your handler as unauthenticated/anonymous so that you can manually validate the JWT to ensure proper authorization.
The form can be dynamically created and immediately destroyed so that it is properly cleaned up (note: this can be done in plain JS, but JQuery is used here for clarity) -
function DownloadWithJwtViaFormPost(url, id, token) {
var jwtInput = $('<input type="hidden" name="jwtToken">').val(token);
var idInput = $('<input type="hidden" name="id">').val(id);
$('<form method="post" target="_blank"></form>')
.attr("action", url)
.append(jwtInput)
.append(idInput)
.appendTo('body')
.submit()
.remove();
}
Just add any extra data you need to submit as hidden inputs and make sure they are appended to the form.
I would generate tokens for download.
Within angular make an authenticated request to obtain a temporary token (say an hour) then add it to the url as a get parameter. This way you can download files in any way you like (window.open ...)
Pure JS version of James' answer
function downloadFile (url, token) {
let form = document.createElement('form')
form.method = 'post'
form.target = '_blank'
form.action = url
form.innerHTML = '<input type="hidden" name="jwtToken" value="' + token + '">'
console.log('form:', form)
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With