Come gestire i download di file con l'autenticazione basata su JWT?


116

Sto scrivendo una webapp in Angular dove l'autenticazione è gestita da un token JWT, il che significa che ogni richiesta ha un'intestazione "Authentication" con tutte le informazioni necessarie.

Funziona bene per le chiamate REST, ma non capisco come dovrei gestire i link di download per i file ospitati sul back-end (i file risiedono sullo stesso server in cui sono ospitati i servizi web).

Non posso utilizzare <a href='...'/>collegamenti regolari poiché non portano alcuna intestazione e l'autenticazione fallirà. Lo stesso vale per i vari incantesimi di window.open(...).

Alcune soluzioni a cui ho pensato:

  1. Genera un collegamento per il download temporaneo non protetto sul server
  2. Passa le informazioni di autenticazione come parametro URL e gestisci manualmente il caso
  3. Ottieni i dati tramite XHR e salva il file lato client.

Tutto quanto sopra è meno che soddisfacente.

1 è la soluzione che sto usando in questo momento. Non mi piace per due motivi: primo non è l'ideale dal punto di vista della sicurezza, secondo funziona ma richiede parecchio lavoro soprattutto sul server: per scaricare qualcosa devo chiamare un servizio che genera un nuovo "random "url, lo memorizza da qualche parte (possibilmente sul DB) per un po 'di tempo e lo restituisce al client. Il client ottiene l'URL e usa window.open o simile con esso. Quando richiesto, il nuovo URL dovrebbe verificare se è ancora valido e quindi restituire i dati.

2 sembra almeno altrettanto lavoro.

3 sembra un sacco di lavoro, anche usando le librerie disponibili, e molti potenziali problemi. (Avrei bisogno di fornire la mia barra di stato del download, caricare l'intero file in memoria e quindi chiedere all'utente di salvare il file localmente).

Il compito sembra piuttosto semplice, quindi mi chiedo se c'è qualcosa di molto più semplice che posso usare.

Non sto necessariamente cercando una soluzione "alla maniera angolare". Javascript normale andrebbe bene.


Da remoto intendi che i file scaricabili si trovano su un dominio diverso rispetto all'app Angular? Controlli il telecomando (hai accesso per modificarne il backend) o no?
robertjd

Voglio dire che i dati del file non si trovano sul client (browser); il file è ospitato sullo stesso dominio e ho il controllo del backend. Aggiornerò la domanda per renderla meno ambigua.
Marco Righele

La difficoltà dell'opzione 2 dipende dal tuo backend. Se puoi dire al tuo backend di controllare la stringa di query oltre all'intestazione di autorizzazione per il JWT quando passa attraverso il livello di autenticazione, il gioco è fatto. Quale backend stai usando?
Tecnezio

Risposte:


47

Ecco un modo per scaricarlo sul client utilizzando l'attributo download , l'API fetch e URL.createObjectURL . Dovresti recuperare il file usando il tuo JWT, convertire il payload in un blob, inserire il blob in un objectURL, impostare l'origine di un anchor tag su quell'objectURL e fare clic su quell'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);
    });

Il valore downloaddell'attributo sarà l'eventuale nome del file. Se lo desideri, puoi estrarre un nome file previsto dall'intestazione della risposta sulla disposizione del contenuto come descritto in altre risposte .


1
Continuo a chiedermi perché nessuno prende in considerazione questa risposta. È semplice e poiché viviamo nel 2017, il supporto della piattaforma è abbastanza buono.
Rafal Pastuszak

1
Ma il supporto iosSafari per l'attributo di download sembra piuttosto rosso :(
Martin Cremer

1
Questo ha funzionato bene per me in Chrome. Per firefox ha funzionato dopo aver aggiunto l'ancora al documento: document.body.appendChild (anchor); Non ho trovato alcuna soluzione per Edge ...
Tompi

12
Questa soluzione funziona, ma questa soluzione gestisce i problemi di UX con file di grandi dimensioni? Se a volte devo scaricare un file da 300 MB, potrebbe essere necessario del tempo per scaricarlo prima di fare clic sul collegamento e inviarlo al gestore di download del browser. Potremmo dedicare lo sforzo a utilizzare l'API fetch-progress e costruire la nostra interfaccia utente di avanzamento del download .. ma poi c'è anche la discutibile pratica di caricare un file da 300 MB in js-land (in memoria?) Per passarlo semplicemente al download manager.
scvnc

1
@Tompi anch'io non sono riuscito a farlo funzionare per Edge e IE
zappa

34

Tecnica

Basandomi su questo consiglio di Matias Woloski di Auth0, noto evangelista di JWT, ho risolto il problema generando una richiesta firmata con Hawk .

Citando Woloski:

Il modo in cui risolvi questo problema è generando una richiesta firmata come fa AWS, ad esempio.

Ecco un esempio di questa tecnica, utilizzata per i collegamenti di attivazione.

backend

Ho creato un'API per firmare i miei URL di download:

Richiesta:

POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}

Risposta:

{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}

Con un URL firmato, possiamo ottenere il file

Richiesta:

GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c

Risposta:

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

frontend (di jojoyuji )

In questo modo puoi fare tutto con un solo clic dell'utente:

function clickedOnDownloadButton() {

  postToSignWithAuthorizationHeader({
    url: 'https://path.to/protected.file'
  }).then(function(signed) {
    window.location = signed.url;
  });

}

2
Questo è interessante ma non capisco come sia diverso, dal punto di vista della sicurezza, dall'opzione n. 2 dell'OP (token come parametro della stringa di query). In realtà, posso immaginare che la richiesta firmata potrebbe essere più restrittiva, ovvero consentire solo l'accesso a un particolare endpoint. Ma il numero 2 dell'OP sembra più facile / pochi passaggi, cosa c'è di sbagliato in questo?
Tyler Collier

4
A seconda del server Web, l'URL completo potrebbe essere registrato nei suoi file di registro. Potresti non volere che il tuo personale IT abbia accesso a tutti i token.
Ezequias Dinella

2
Inoltre, l'URL con la stringa di query verrà salvato nella cronologia dell'utente, consentendo ad altri utenti della stessa macchina di accedere all'URL.
Ezequias Dinella

1
Infine, ciò che lo rende molto insicuro è che l'URL viene inviato nell'intestazione Referer di tutte le richieste per qualsiasi risorsa, anche risorse di terze parti. Quindi, se utilizzi Google Analytics, ad esempio, invierai a Google il token URL e tutto a loro.
Ezequias Dinella

1
Questo testo è stato preso da qui: stackoverflow.com/questions/643355/…
Ezequias Dinella

10

Un'alternativa agli approcci esistenti "fetch / createObjectURL" e "download-token" già menzionati è un modulo POST standard che si rivolge a una nuova finestra . Una volta che il browser ha letto l'intestazione dell'allegato nella risposta del server, chiuderà la nuova scheda e inizierà il download. Questo stesso approccio funziona bene anche per la visualizzazione di una risorsa come un PDF in una nuova scheda.

Questo ha un supporto migliore per i browser meno recenti ed evita di dover gestire un nuovo tipo di token. Questo avrà anche un supporto a lungo termine migliore rispetto all'autenticazione di base sull'URL, poiché il supporto per nome utente / password sull'URL viene rimosso dai browser .

Sul lato client usiamo target="_blank"per evitare la navigazione anche in casi di errore, cosa particolarmente importante per le SPA (app a pagina singola).

L'avvertenza principale è che la convalida JWT lato server deve ottenere il token dai dati POST e non dall'intestazione . Se il tuo framework gestisce automaticamente l'accesso ai gestori di route utilizzando l'intestazione di autenticazione, potresti dover contrassegnare il tuo gestore come non autenticato / anonimo in modo da poter convalidare manualmente il JWT per garantire la corretta autorizzazione.

Il modulo può essere creato dinamicamente e immediatamente distrutto in modo che venga adeguatamente ripulito (nota: questo può essere fatto in semplice JS, ma JQuery è usato qui per chiarezza) -

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();
}

Aggiungi semplicemente i dati extra che devi inviare come input nascosti e assicurati che siano aggiunti al modulo.


1
Credo che questa soluzione sia notevolmente sottovalutata. È facile, pulito e funziona perfettamente.
Yura Fedoriv

6

Genererei token per il download.

All'interno di angular fai una richiesta autenticata per ottenere un token temporaneo (diciamo un'ora) quindi aggiungilo all'URL come parametro get. In questo modo puoi scaricare i file nel modo che preferisci (window.open ...)


2
Questa è la soluzione che sto usando per ora, ma non sono soddisfatto perché è un bel po 'di lavoro e spero che ci sia una soluzione migliore "là fuori" ...
Marco Righele

3
Penso che questa sia la soluzione più pulita disponibile e non riesco a vedere molto lavoro lì. Ma sceglierei un tempo di validità inferiore del token (ad esempio 3 minuti) o lo renderei un token una tantum mantenendo un elenco dei token sul server ed eliminare i token usati (non accettando i token che non sono nella mia lista ).
nabinca

5

Una soluzione aggiuntiva: utilizzare l'autenticazione di base. Sebbene richieda un po 'di lavoro sul back-end, i token non saranno visibili nei log e non sarà necessario implementare la firma dell'URL.


Dalla parte del cliente

Un URL di esempio potrebbe essere:

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

Esempio con token fittizio:

http://jwt:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwIiwibmFtZSI6IiIsImlhdCI6MH0.KsKmQOZM-jcy4l_7NFsv1lWfpH8ofniVCv75ZRQrWno@some.url/file/35/download

Puoi quindi inserirlo <a href="...">o window.open("...")- il browser gestisce il resto.


Lato server

L'implementazione qui dipende da te e dipende dalla configurazione del tuo server: non è molto diverso dall'utilizzo del ?token=parametro di query.

Utilizzando Laravel, ho seguito la strada più semplice e ho trasformato la password di autenticazione di base Authorization: Bearer <...>nell'intestazione JWT , lasciando che il normale middleware di autenticazione gestisse il resto:

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.
        $request->headers->set(
            'Authorization',
            '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.');
    }
}

Questo approccio sembra promettente, ma non vedo un modo per ottenere l'accesso al token JWT in questo modo. Puoi indicarmi qualche risorsa su come il server analizza questo strano URL e dove accedere al valore del token jwt?
Jiri Vetyska

1
@JiriVetyska LOL PROMISING? Il token è ancora più chiaro che passarlo nelle intestazioni ahahahha
Liquid Core
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.