web-dev-qa-db-fra.com

Comment gérer les téléchargements de fichiers avec l'authentification basée sur JWT?

J'écris une webapp dans Angular où l'authentification est gérée par un jeton JWT, ce qui signifie que chaque demande a un en-tête "Authentification" avec toutes les informations nécessaires.

Cela fonctionne bien pour les appels REST, mais je ne comprends pas comment gérer les liens de téléchargement pour les fichiers hébergés sur le backend (les fichiers résident sur le même serveur où sont hébergés les services Web).

Je ne peux pas utiliser les liens normaux <a href='...'/> Car ils ne portent aucun en-tête et l'authentification échouera. Idem pour les différentes incantations de window.open(...).

Quelques solutions auxquelles j'ai pensé:

  1. Générer un lien de téléchargement temporaire non sécurisé sur le serveur
  2. Transmettez les informations d'authentification sous forme de paramètre url et gérez manuellement le cas.
  3. Obtenez les données via XHR et enregistrez le fichier côté client.

Tout ce qui précède est moins que satisfaisant.

1 est la solution que j'utilise actuellement. Je ne l’aime pas pour deux raisons: premièrement, ce n’est pas idéal du point de vue de la sécurité, deuxièmement, cela fonctionne, mais cela demande beaucoup de travail, en particulier sur le serveur: pour télécharger quelque chose, il faut que j’appelle un service qui génère un nouveau "hasard". "url, le stocke quelque part (éventuellement sur la base de données) pendant un certain temps et le renvoie au client. Le client obtient l'URL et utilise window.open ou similaire avec elle. Lorsque cela est demandé, la nouvelle URL doit vérifier si elle est toujours valide, puis renvoyer les données.

2 semble au moins autant de travail.

3 semble faire beaucoup de travail, même en utilisant les bibliothèques disponibles, et beaucoup de problèmes potentiels. (J'aurais besoin de fournir ma propre barre d'état de téléchargement, de charger le fichier entier en mémoire, puis de demander à l'utilisateur de sauvegarder le fichier localement).

La tâche semble cependant assez élémentaire, alors je me demande s’il existe quelque chose de beaucoup plus simple que je puisse utiliser.

Je ne cherche pas nécessairement une solution "the Angular way"). Javascript normal conviendrait.

94
Marco Righele

Voici un moyen de le télécharger sur le client en utilisant l'attribut download , l'API d'extraction , et RL.createObjectURL . Vous récupéreriez le fichier à l'aide de votre JWT, convertiriez la charge en un blob, placeriez le blob dans un objectURL, définir la source d'une balise d'ancrage sur cet objectURL et cliquez sur celui-ci en 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);
    });

La valeur de l'attribut download sera le nom de fichier éventuel. Si vous le souhaitez, vous pouvez extraire un nom de fichier voulu de l'en-tête de réponse de disposition de contenu comme décrit dans d'autres réponses .

36
Technetium

Technique

Basé sur ce conseil de Matias Woloski de Auth0, évangéliste connu de JWT, je l'ai résolu en générant une demande signée avec Hawk .

Citant Woloski:

Pour résoudre ce problème, vous devez générer une demande signée, comme le fait AWS, par exemple.

Vous avez ici un exemple de cette technique, utilisée pour les liens d'activation.

backend

J'ai créé une API pour signer mes URL de téléchargement:

Demande:

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

Réponse:

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

Avec une URL signée, nous pouvons obtenir le fichier

Demande:

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

Réponse:

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

frontend (by jojoyuji )

De cette façon, vous pouvez tout faire sur un seul utilisateur, cliquez sur:

function clickedOnDownloadButton() {

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

}
29
Ezequias Dinella

Je générerais des jetons pour le téléchargement.

Dans angular faites une demande authentifiée pour obtenir un jeton temporaire (disons une heure) puis ajoutez-le à l’url en tant que paramètre get. Vous pourrez ainsi télécharger des fichiers comme vous le souhaitez (fenêtre. ouvert ...)

6
Fred

Une solution supplémentaire: utiliser l'authentification de base. Bien que cela nécessite un peu de travail sur le backend, les jetons ne seront pas visibles dans les journaux et aucune signature d'URL ne devra être implémentée.


Côté client

Un exemple d'URL pourrait être:

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

Exemple avec jeton factice:

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

Vous pouvez alors le placer dans <a href="..."> Ou window.open("...") - le navigateur se charge du reste.


Du côté serveur

L'implémentation dépend ici de vous et dépend de la configuration de votre serveur. L'utilisation du paramètre de requête ?token= N'est pas très différente.

En utilisant Laravel, j'ai emprunté la voie facile et transformé le mot de passe de l'authentification de base en en-tête JWT Authorization: Bearer <...>, Laissant le middleware d'authentification normal gérer le reste:

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.');
    }
}
3
AlbinoDrought