Jak obsługiwać pobieranie plików z uwierzytelnianiem opartym na JWT?

116

Piszę aplikację internetową w Angular, w której uwierzytelnianie jest obsługiwane przez token JWT, co oznacza, że ​​każde żądanie ma nagłówek „Authentication” ze wszystkimi niezbędnymi informacjami.

Działa to dobrze w przypadku wywołań REST, ale nie rozumiem, jak mam obsługiwać łącza pobierania plików hostowanych na zapleczu (pliki znajdują się na tym samym serwerze, na którym hostowane są usługi sieciowe).

Nie mogę używać zwykłych <a href='...'/>linków, ponieważ nie mają one żadnego nagłówka, a uwierzytelnianie się nie powiedzie. To samo dotyczy różnych zaklęć window.open(...).

Niektóre rozwiązania, o których myślałem:

  1. Wygeneruj tymczasowe niezabezpieczone łącze pobierania na serwerze
  2. Przekaż informacje uwierzytelniające jako parametr adresu URL i ręcznie obsłuż sprawę
  3. Pobierz dane przez XHR i zapisz plik po stronie klienta.

Wszystkie powyższe są mniej niż zadowalające.

1 to rozwiązanie, którego teraz używam. Nie podoba mi się to z dwóch powodów: po pierwsze nie jest to idealne rozwiązanie z punktu widzenia bezpieczeństwa, po drugie działa, ale wymaga sporo pracy, szczególnie na serwerze: aby coś pobrać, muszę zadzwonić do usługi, która generuje nowy "losowy „url, przechowuje go gdzieś (prawdopodobnie w bazie danych) przez jakiś czas i zwraca do klienta. Klient otrzymuje adres URL i używa z nim window.open lub podobnego. Na żądanie nowy adres URL powinien sprawdzić, czy nadal jest ważny, a następnie zwrócić dane.

2 wydaje się co najmniej tyle pracy.

3 wydaje się dużo pracy, nawet przy użyciu dostępnych bibliotek i wiele potencjalnych problemów. (Musiałbym zapewnić własny pasek stanu pobierania, załadować cały plik do pamięci, a następnie poprosić użytkownika o zapisanie pliku lokalnie).

Zadanie wydaje się jednak dość podstawowe, więc zastanawiam się, czy jest coś znacznie prostszego, czego mógłbym użyć.

Niekoniecznie szukam rozwiązania „po kątach”. Zwykły Javascript byłby w porządku.

Marco Righele
źródło
Czy mówiąc zdalnie, masz na myśli, że pliki do pobrania znajdują się w innej domenie niż aplikacja Angular? Kontrolujesz pilota (masz dostęp do modyfikowania jego zaplecza) czy nie?
robertjd
Mam na myśli, że dane pliku nie znajdują się na kliencie (przeglądarce); plik jest hostowany w tej samej domenie i mam kontrolę nad zapleczem. Zaktualizuję pytanie, aby było mniej niejednoznaczne.
Marco Righele
Trudność opcji 2 zależy od twojego zaplecza. Jeśli możesz powiedzieć swojemu zapleczu, aby sprawdzał ciąg zapytania oraz nagłówek autoryzacji tokena JWT, gdy przechodzi przez warstwę uwierzytelniania, to wszystko. Którego zaplecza używasz?
Technetium

Odpowiedzi:

47

Oto sposób, aby pobrać go na kliencie za pomocą atrybutu pobierania , pobranie informacji API i URL.createObjectURL . Pobrałbyś plik za pomocą tokena JWT, przekonwertowałbyś ładunek na obiekt blob, umieścił obiekt blob w objectURL, ustawił źródło tagu kotwicy na ten objectURL i kliknął ten objectURL w 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);
    });

Wartością downloadatrybutu będzie ostateczna nazwa pliku. Jeśli chcesz, możesz wydobyć zamierzoną nazwę pliku z nagłówka odpowiedzi dyspozycji dyspozycji treści, jak opisano w innych odpowiedziach .

Technet
źródło
1
Zastanawiam się, dlaczego nikt nie bierze pod uwagę tej odpowiedzi. To proste, a ponieważ żyjemy w 2017 roku, obsługa platformy jest dość dobra.
Rafal Pastuszak
1
Ale obsługa iosSafari dla atrybutu pobierania wygląda całkiem czerwono :(
Martin Cremer
1
To działało dobrze dla mnie w chromie. W przypadku przeglądarki Firefox zadziałało po dodaniu kotwicy do dokumentu: document.body.appendChild (kotwica); Nie znalazłem żadnego rozwiązania dla Edge ...
Tompi
12
To rozwiązanie działa, ale czy to rozwiązanie radzi sobie z problemami z UX w przypadku dużych plików? Jeśli muszę czasami pobrać plik o rozmiarze 300 MB, może to zająć trochę czasu, zanim kliknę łącze i wyślę go do menedżera pobierania przeglądarki. Moglibyśmy poświęcić wysiłek na użycie api pobierania-postępu i zbudować własny interfejs postępu pobierania ... ale jest też wątpliwa praktyka ładowania pliku 300 MB do js-land (w pamięci?), Aby po prostu przekazać go do pobrania menedżer.
scvnc
1
@Tompi Ja też nie mogłem tego zrobić dla Edge i IE
zappa
34

Technika

Na podstawie rady Matiasa Wołoskiego z Auth0, znanego ewangelisty JWT, rozwiązałem go, generując podpisaną prośbę z Hawkiem .

Cytując Woloskiego:

Sposób rozwiązania tego problemu polega na wygenerowaniu podpisanego żądania, tak jak na przykład robi to AWS.

Tutaj masz przykład tej techniki, używanej do linków aktywacyjnych.

zaplecze

Stworzyłem API do podpisywania moich adresów URL pobierania:

Żądanie:

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

Odpowiedź:

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

Za pomocą podpisanego adresu URL możemy pobrać plik

Żądanie:

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

Odpowiedź:

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

frontend (przez jojoyuji )

W ten sposób możesz to wszystko zrobić jednym kliknięciem użytkownika:

function clickedOnDownloadButton() {

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

}
Ezequias Dinella
źródło
2
To jest fajne, ale nie rozumiem, jak różni się, z punktu widzenia bezpieczeństwa, od opcji nr 2 OP (token jako parametr ciągu zapytania). Właściwie mogę sobie wyobrazić, że podpisane żądanie mogłoby być bardziej restrykcyjne, tj. Po prostu mieć dostęp do określonego punktu końcowego. Ale OP nr 2 wydaje się łatwiejszy / mniej kroków, co w tym złego?
Tyler Collier,
4
W zależności od serwera WWW pełny adres URL może zostać zarejestrowany w plikach dziennika. Możesz nie chcieć, aby pracownicy IT mieli dostęp do wszystkich tokenów.
Ezequias Dinella
2
Ponadto adres URL z ciągiem zapytania zostanie zapisany w historii użytkownika, umożliwiając innym użytkownikom tego samego komputera dostęp do adresu URL.
Ezequias Dinella
1
Wreszcie, co sprawia, że ​​jest to bardzo niebezpieczne, adres URL jest wysyłany w nagłówku Referer wszystkich żądań dowolnego zasobu, nawet zasobów stron trzecich. Jeśli więc korzystasz na przykład z Google Analytics, wyślesz do Google token adresu URL i wszystkie do nich.
Ezequias Dinella
1
Ten tekst został wzięty stąd: stackoverflow.com/questions/643355/ ...
Ezequias Dinella
10

Alternatywą dla istniejących już podejść „fetch / createObjectURL” i „download-token” jest standardowy formularz POST, który jest przeznaczony dla nowego okna . Gdy przeglądarka odczyta nagłówek załącznika w odpowiedzi serwera, zamknie nową kartę i rozpocznie pobieranie. To samo podejście działa również dobrze przy wyświetlaniu zasobu, takiego jak plik PDF, w nowej karcie.

Zapewnia to lepszą obsługę starszych przeglądarek i pozwala uniknąć konieczności zarządzania nowym typem tokena. Zapewni to również lepszą obsługę długoterminową niż podstawowe uwierzytelnianie adresu URL, ponieważ obsługa nazwy użytkownika / hasła w adresie URL jest usuwana przez przeglądarki .

Po stronie klienta używamy, target="_blank"aby uniknąć nawigacji nawet w przypadkach awarii, co jest szczególnie ważne w przypadku SPA (aplikacji jednostronicowych).

Głównym zastrzeżeniem jest to, że weryfikacja JWT po stronie serwera musi pobrać token z danych POST, a nie z nagłówka . Jeśli struktura zarządza dostępem do programów obsługi tras automatycznie przy użyciu nagłówka uwierzytelniania, może być konieczne oznaczenie procedury obsługi jako nieuwierzytelnionej / anonimowej, aby można było ręcznie sprawdzić poprawność tokena JWT w celu zapewnienia właściwej autoryzacji.

Formularz może być dynamicznie tworzony i natychmiast niszczony, aby został odpowiednio wyczyszczony (uwaga: można to zrobić w zwykłym JS, ale dla przejrzystości zastosowano tutaj JQuery) -

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

Po prostu dodaj wszelkie dodatkowe dane, które musisz przesłać jako ukryte dane wejściowe i upewnij się, że zostały dołączone do formularza.

James
źródło
1
Uważam, że to rozwiązanie jest bardzo niedoceniane. Jest łatwy, czysty i działa doskonale.
Yura Fedoriv
6

Generowałbym tokeny do pobrania.

W ramach angular wyślij uwierzytelnione żądanie uzyskania tymczasowego tokena (powiedzmy na godzinę), a następnie dodaj go do adresu URL jako parametr get. W ten sposób możesz pobierać pliki w dowolny sposób (window.open ...)

Fred
źródło
2
To jest rozwiązanie, którego używam na razie, ale nie jestem z niego zadowolony, ponieważ wymaga sporo pracy i mam nadzieję, że istnieje lepsze rozwiązanie "tam" ...
Marco Righele
3
Myślę, że to najczystsze dostępne rozwiązanie i nie widzę tam dużo pracy. Ale wybrałbym albo mniejszy czas ważności tokena (np. 3 minuty), albo uczyniłbym go jednorazowym tokenem, utrzymując listę tokenów na serwerze i usuwając używane tokeny (nie akceptując tokenów, których nie ma na mojej liście ).
nabinca
5

Dodatkowe rozwiązanie: użycie podstawowego uwierzytelnienia. Chociaż wymaga to trochę pracy na zapleczu, tokeny nie będą widoczne w dziennikach i nie będzie trzeba zaimplementować podpisywania adresów URL.


Strona klienta

Przykładowy adres URL może wyglądać tak:

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

Przykład z fałszywym tokenem:

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

Następnie możesz to włożyć <a href="...">lub window.open("...")- przeglądarka zajmie się resztą.


Po stronie serwera

Wdrożenie tutaj zależy od Ciebie i zależy od konfiguracji serwera - nie różni się zbytnio od użycia ?token=parametru zapytania.

Korzystając z Laravel, poszedłem na łatwą ścieżkę i przekształciłem podstawowe hasło uwierzytelniające w Authorization: Bearer <...>nagłówek JWT , pozwalając normalnemu oprogramowaniu pośredniczącemu uwierzytelniania zająć się resztą:

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.');
    }
}
AlbinoDrought
źródło
To podejście wydaje się obiecujące, ale nie widzę sposobu na uzyskanie w ten sposób dostępu do tokena JWT. Czy możesz wskazać mi jakieś zasoby, w jaki sposób serwer analizuje ten dziwny adres URL i gdzie uzyskać dostęp do wartości tokena jwt?
Jiri Vetyska
1
@JiriVetyska LOL OBIECUJE? Token jest jeszcze bardziej przejrzysty niż przekazywanie go w nagłówkach ahahahha
Liquid Core