Bagaimana menangani unduhan file dengan otentikasi berbasis JWT?

116

Saya menulis aplikasi web di Angular di mana autentikasi ditangani oleh token JWT, yang berarti bahwa setiap permintaan memiliki header "Autentikasi" dengan semua informasi yang diperlukan.

Ini berfungsi dengan baik untuk panggilan REST, tetapi saya tidak mengerti bagaimana saya harus menangani tautan unduhan untuk file yang dihosting di backend (file berada di server yang sama tempat layanan web dihosting).

Saya tidak dapat menggunakan <a href='...'/>tautan biasa karena mereka tidak akan membawa header apa pun dan autentikasi akan gagal. Sama untuk berbagai mantra window.open(...).

Beberapa solusi yang saya pikirkan:

  1. Buat tautan unduhan sementara yang tidak aman di server
  2. Teruskan informasi otentikasi sebagai parameter url dan tangani kasus secara manual
  3. Dapatkan data melalui XHR dan simpan sisi klien file.

Semua hal di atas kurang memuaskan.

1 adalah solusi yang saya gunakan sekarang. Saya tidak menyukainya karena dua alasan: pertama ini tidak ideal dari segi keamanan, kedua berfungsi tetapi memerlukan cukup banyak pekerjaan terutama di server: untuk mengunduh sesuatu saya perlu memanggil layanan yang menghasilkan "acak "url, menyimpannya di suatu tempat (mungkin di DB) untuk beberapa waktu, dan mengembalikannya ke klien. Klien mendapatkan url, dan menggunakan window.open atau serupa dengannya. Saat diminta, url baru harus memeriksa apakah masih valid, lalu mengembalikan datanya.

2 tampaknya setidaknya banyak pekerjaan.

3 tampaknya banyak pekerjaan, bahkan menggunakan perpustakaan yang tersedia, dan banyak masalah potensial. (Saya perlu menyediakan bilah status unduhan saya sendiri, memuat seluruh file dalam memori dan kemudian meminta pengguna untuk menyimpan file secara lokal).

Tugasnya tampaknya cukup mendasar, jadi saya bertanya-tanya apakah ada sesuatu yang lebih sederhana yang dapat saya gunakan.

Saya belum tentu mencari solusi "cara Angular". Javascript biasa akan baik-baik saja.

Marco Righele
sumber
Maksud Anda, apakah file yang dapat diunduh berada di domain yang berbeda dari aplikasi Angular? Apakah Anda mengontrol remote (memiliki akses untuk memodifikasi backendnya) atau tidak?
robertjd
Maksud saya, data file tidak ada di klien (browser); file tersebut di-host di domain yang sama dan saya memiliki kendali atas backend. Saya akan memperbarui pertanyaan agar tidak terlalu ambigu.
Marco Righele
Kesulitan opsi 2 bergantung pada backend Anda. Jika Anda dapat memberi tahu backend Anda untuk memeriksa string kueri selain header otorisasi untuk JWT saat melewati lapisan otentikasi, Anda sudah selesai. Backend mana yang Anda gunakan?
Teknesium

Jawaban:

47

Berikut cara untuk mendownloadnya di klien menggunakan atribut download , fetch API , dan URL.createObjectURL . Anda akan mengambil file menggunakan JWT, mengubah payload menjadi blob, meletakkan blob tersebut menjadi objectURL, menyetel sumber tag anchor ke objectURL itu, dan mengklik objectURL itu dalam 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);
    });

Nilai downloadatribut akan menjadi nama file akhirnya. Jika diinginkan, Anda dapat menambang nama file yang diinginkan dari header respons disposisi konten seperti yang dijelaskan dalam jawaban lain .

Teknesium
sumber
1
Saya terus bertanya-tanya mengapa tidak ada yang mempertimbangkan tanggapan ini. Sederhana dan karena kami hidup di tahun 2017, dukungan platformnya cukup bagus.
Rafal Pastuszak
1
Tetapi dukungan iosSafari untuk atribut unduhan terlihat sangat merah :(
Martin Cremer
1
Ini bekerja dengan baik untuk saya di chrome. Untuk firefox itu bekerja setelah saya menambahkan jangkar ke dokumen: document.body.appendChild (jangkar); Tidak menemukan solusi untuk Edge ...
Tompi
12
Solusi ini berfungsi tetapi apakah solusi ini menangani masalah UX dengan file besar? Jika terkadang saya perlu mengunduh file 300MB, mungkin perlu beberapa saat untuk mengunduh sebelum mengeklik tautan dan mengirimkannya ke pengelola unduhan browser. Kita bisa menghabiskan usaha menggunakan fetch-progress api dan membangun UI kemajuan unduhan kita sendiri .. tapi kemudian ada juga praktik yang dipertanyakan untuk memuat file 300mb ke dalam js-land (dalam memori?) Hanya untuk menyerahkannya ke unduhan Pengelola.
scvnc
1
@Tompi saya juga tidak bisa membuat ini berfungsi untuk Edge dan IE
zappa
34

Teknik

Berdasarkan saran Matias Woloski dari Auth0, penginjil JWT yang terkenal ini, saya menyelesaikannya dengan membuat permintaan yang ditandatangani dengan Hawk .

Mengutip Woloski:

Cara Anda menyelesaikannya adalah dengan membuat permintaan yang ditandatangani seperti yang dilakukan AWS, misalnya.

Di sini Anda memiliki contoh teknik ini, digunakan untuk tautan aktivasi.

backend

Saya membuat API untuk menandatangani url unduhan saya:

Permintaan:

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

Tanggapan:

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

Dengan URL yang ditandatangani, kita bisa mendapatkan file tersebut

Permintaan:

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

Tanggapan:

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

frontend (oleh jojoyuji )

Dengan cara ini Anda dapat melakukan semuanya dengan satu klik pengguna:

function clickedOnDownloadButton() {

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

}
Ezequias Dinella
sumber
2
Ini keren tetapi saya tidak mengerti bagaimana perbedaannya, dari perspektif keamanan, dari opsi OP # 2 (token sebagai parameter string kueri). Sebenarnya, saya dapat membayangkan bahwa permintaan yang ditandatangani dapat lebih dibatasi, yaitu hanya diizinkan untuk mengakses titik akhir tertentu. Tapi OP # 2 sepertinya lebih mudah / lebih sedikit langkah, apa yang salah dengan itu?
Tyler Collier
4
Bergantung pada server web Anda, URL lengkap mungkin masuk ke file log-nya. Anda mungkin tidak ingin orang IT Anda memiliki akses ke semua token.
Ezequias Dinella
2
Selain itu, URL dengan string kueri akan disimpan dalam riwayat pengguna Anda, memungkinkan pengguna lain dari mesin yang sama untuk mengakses URL.
Ezequias Dinella
1
Terakhir dan yang membuatnya sangat tidak aman adalah, URL dikirim di header Referer dari semua permintaan untuk sumber daya apa pun, bahkan sumber daya pihak ketiga. Jadi jika Anda menggunakan Google Analytics misalnya, Anda akan mengirimkan token URL ke Google dan semuanya kepada mereka.
Ezequias Dinella
1
Teks ini diambil dari sini: stackoverflow.com/questions/643355/…
Ezequias Dinella
10

Alternatif untuk pendekatan "fetch / createObjectURL" dan "download-token" yang telah disebutkan adalah Formulir POST standar yang menargetkan jendela baru . Setelah browser membaca header lampiran pada respons server, itu akan menutup tab baru dan mulai mengunduh. Pendekatan yang sama ini juga bekerja dengan baik untuk menampilkan sumber daya seperti PDF di tab baru.

Ini memiliki dukungan yang lebih baik untuk browser lama dan menghindari keharusan mengelola jenis token baru. Ini juga akan memiliki dukungan jangka panjang yang lebih baik daripada autentikasi dasar pada URL, karena dukungan untuk nama pengguna / kata sandi pada url sedang dihapus oleh browser .

Di sisi klien kami menggunakan target="_blank"untuk menghindari navigasi bahkan dalam kasus kegagalan, yang sangat penting untuk SPA (aplikasi satu halaman).

Peringatan utamanya adalah validasi JWT sisi server harus mendapatkan token dari data POST dan bukan dari header . Jika framework Anda mengelola akses ke penangan rute secara otomatis menggunakan header Authentication, Anda mungkin perlu menandai penangan Anda sebagai tidak diautentikasi / anonim sehingga Anda dapat memvalidasi JWT secara manual untuk memastikan otorisasi yang tepat.

Formulir dapat dibuat secara dinamis dan segera dihancurkan sehingga dibersihkan dengan benar (catatan: ini dapat dilakukan dalam JS biasa, tetapi JQuery digunakan di sini untuk kejelasan) -

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

Cukup tambahkan data tambahan yang perlu Anda kirimkan sebagai input tersembunyi dan pastikan data tersebut ditambahkan ke formulir.

James
sumber
1
Saya yakin solusi ini sangat diabaikan. Mudah, bersih, dan berfungsi dengan sempurna.
Yura Fedoriv
6

Saya akan membuat token untuk diunduh.

Dalam angular buat permintaan yang diautentikasi untuk mendapatkan token sementara (katakanlah satu jam) lalu tambahkan ke url sebagai parameter get. Dengan cara ini Anda dapat mengunduh file dengan cara apa pun yang Anda suka (window.open ...)

Fred
sumber
2
Ini adalah solusi yang saya gunakan untuk saat ini, tetapi saya tidak puas dengan itu karena ini cukup banyak pekerjaan dan saya berharap ada solusi yang lebih baik "di luar sana" ...
Marco Righele
3
Saya pikir ini adalah solusi terbersih yang tersedia dan saya tidak dapat melihat banyak pekerjaan di sana. Tetapi saya akan memilih waktu validitas token yang lebih kecil (misalnya 3 menit) atau menjadikannya token satu kali dengan menyimpan daftar token di server dan menghapus token bekas (tidak menerima token yang tidak ada dalam daftar saya ).
nabinca
5

Solusi tambahan: menggunakan otentikasi dasar. Meskipun memerlukan sedikit pekerjaan di backend, token tidak akan terlihat di log dan tidak ada penandatanganan URL yang harus diterapkan.


Sisi klien

Contoh URL bisa jadi:

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

Contoh dengan token dummy:

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

Anda kemudian dapat memasukkan ini ke dalam <a href="...">atau window.open("...")- browser menangani sisanya.


Sisi server

Penerapannya di sini terserah Anda, dan bergantung pada penyiapan server Anda - tidak terlalu jauh berbeda dengan menggunakan ?token=parameter kueri.

Dengan menggunakan Laravel, saya mengambil rute yang mudah dan mengubah kata sandi otentikasi dasar menjadi Authorization: Bearer <...>header JWT , membiarkan middleware otentikasi normal menangani sisanya:

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.');
    }
}
AlbinoKekeringan
sumber
Pendekatan ini tampaknya menjanjikan, tetapi saya tidak melihat cara untuk mendapatkan akses ke token JWT dengan cara ini. Dapatkah Anda mengarahkan saya ke beberapa sumber daya bagaimana server mem-parse url aneh ini dan di mana mengakses nilai token jwt?
Jiri Vetyska
1
@JiriVetyska LOL PROMISING? Token ini bahkan lebih jelas daripada meneruskannya di header ahahahha
Liquid Core