How to handle file downloads with JWT based authentication?

I’m writing a webapp in Angular where authentication is handled by a JWT token, meaning that every request has an “Authentication” header with all the necessary information.

This works nicely for REST calls, but I don’t understand how I should handle download links for files hosted on the backend (the files reside on the same server where the webservices are hosted).

I can’t use regular <a href='...'/> links since they won’t carry any header and the authentication will fail. Same for the various incantations of

Some solutions I thought of:

  1. Generate a temporary unsecured download link on the server
  2. Pass the authentication information as an url parameter and manually handle the case
  3. Get the data through XHR and save the file client side.

All of the above are less than satisfactory.

1 is the solution I am using right now. I don’t like it for two reasons: first it is not ideal security-wise, second it works but it requires quite a lot of work especially on the server: to download something I need to call a service that generates a new “random” url, stores it somewhere (possibly on the DB) for a some time, and returns it to the client. The client gets the url, and use or similar with it. When requested, the new url should check if it is still valid, and then return the data.

2 seems at least as much work.

3 seems a lot of work, even using available libraries, and lot of potential issues. (I would need to provide my own download status bar, load the whole file in memory and then ask the user to save the file locally).

The task seems a pretty basic one though, so I’m wondering if there is anything much simpler that I can use.

I’m not necessarily looking for a solution “the Angular way”. Regular Javascript would be fine.

7 thoughts on “How to handle file downloads with JWT based authentication?”

  1. Technique

    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:


    POST /api/sign
    Content-Type: application/json
    Authorization: Bearer...
    {"url": ""}


    {"url": ""}

    With a signed URL, we can get the file




    Content-Type: multipart/mixed; charset="UTF-8"
    Content-Disposition': attachment; filename=protected.file

    frontend (by jojoyuji)

    This way you can do it all on a single user click:

    function clickedOnDownloadButton() {
        url: ''
      }).then(function(signed) {
        window.location = signed.url;
  2. 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");
    let file = '';
    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;
   = 'some-file.pdf';

    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.

  3. 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 ( …)

  4. Pure JS version of James’ answer

    function downloadFile (url, token) {
        let form = document.createElement('form')
        form.method = 'post' = '_blank'
        form.action = url
        form.innerHTML = '<input type="hidden" name="jwtToken" value="' + token + '">'
        console.log('form:', form)
  5. An additional solution: using basic authentication. Although it requires a bit of work on the backend, tokens won’t be visible in logs and no URL signing will have to be implemented.

    Client Side

    An example URL could be:

    http://jwt:<user jwt token>@some.url/file/35/download

    Example with dummy token:


    You can then shove this in <a href="..."> or"...") – the browser handles the rest.

    Server Side

    Implementation here is up to you, and is dependent on your server setup – it’s not too much different from using the ?token= query parameter.

    Using Laravel, I went the easy route and transformed the basic authentication password into the JWT Authorization: Bearer <...> header, letting the normal authentication middleware handle the rest:

    class CarryBasic
         * @param Request $request
         * @param \Closure $next
         * @return mixed
        public function handle($request, \Closure $next)
            // if no basic auth is passed,
            // or the user is not "jwt",
            // send a 401 and trigger the basic auth dialog
            if ($request->getUser() !== 'jwt') {
                return $this->failedBasicResponse();
            // if there _is_ basic auth passed,
            // and the user is JWT,
            // shove the password into the "Authorization: Bearer <...>"
            // header and let the other middleware
            // handle it.
                'Bearer ' . $request->getPassword()
            return $next($request);
         * Get the response for basic authentication.
         * @return void
         * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
        protected function failedBasicResponse()
            throw new UnauthorizedHttpException('Basic', 'Invalid credentials.');
  6. 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)

    Just add any extra data you need to submit as hidden inputs and make sure they are appended to the form.


Leave a Comment