JWTベースの認証でファイルのダウンロードを処理する方法


116

私はAngularでWebアプリケーションを作成していますが、認証はJWTトークンによって処理されます。つまり、すべてのリクエストには、必要なすべての情報を含む「Authentication」ヘッダーがあります。

これはREST呼び出しではうまく機能しますが、バックエンドでホストされているファイル(Webサービスがホストされているのと同じサーバー上にあるファイル)のダウンロードリンクをどのように処理すればよいかわかりません。

<a href='...'/>ヘッダーが含まれておらず、認証が失敗するため、通常のリンクを使用できません。のさまざまな呪文についても同じですwindow.open(...)

私が考えたいくつかの解決策:

  1. サーバー上に一時的な安全でないダウンロードリンクを生成する
  2. 認証情報をurlパラメータとして渡し、ケースを手動で処理します
  3. XHRを介してデータを取得し、ファイルクライアント側に保存します。

上記のすべてが満足できるものではありません。

1は現在使用しているソリューションです。2つの理由で私はそれが好きではありません。1つ目はセキュリティ面で理想的ではない、2つ目は機能しますが、特にサーバーでかなりの作業が必要です。何かをダウンロードするには、新しい「ランダム」を生成するサービスを呼び出す必要があります"url、それをしばらくの間(おそらくDBに)保存し、クライアントに返します。クライアントはURLを取得し、window.openなどを使用します。リクエストされた場合、新しいURLはそれがまだ有効かどうかを確認してから、データを返す必要があります。

2は、少なくとも同じくらいの作業です。

3は、利用可能なライブラリを使用していても、多くの作業と多くの潜在的な問題のようです。(自分のダウンロードステータスバーを用意し、ファイル全体をメモリに読み込んでから、ユーザーにファイルをローカルに保存するよう依頼する必要があります)。

タスクはかなり基本的なもののように思えるので、もっと簡単に使えるものが他にあるかどうか疑問に思っています。

私は必ずしも「Angular way」の解決策を探しているわけではありません。通常のJavascriptで問題ありません。


リモートとは、ダウンロード可能なファイルがAngularアプリとは異なるドメインにあるということですか?リモートを制御していますか(バックエンドを変更するためのアクセス権があります)かどうか。
robertjd

つまり、ファイルデータはクライアント(ブラウザ)上にありません。ファイルは同じドメインでホストされており、私はバックエンドを制御しています。質問があいまいにならないように更新します。
Marco Righele、2015

オプション2の難易度はバックエンドによって異なります。JWTが認証レイヤーを通過するときに、JWTの承認ヘッダーに加えてクエリ文字列をチェックするようにバックエンドに指示できる場合は、これで完了です。どのバックエンドを使用していますか?
Technetium 2017年

回答:


47

これは、ダウンロード属性フェッチAPIURL.createObjectURLを使用してクライアントにダウンロードする方法です。JWTを使用してファイルをフェッチし、ペイロードをblobに変換し、blobをobjectURLに配置し、アンカータグのソースをそのobjectURLに設定し、JavaScriptでそのobjectURLをクリックします。

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

download属性の値は最終的なファイル名になります。必要に応じて、他の回答で説明されているように、コンテンツ処理応答ヘッダーから目的のファイル名をマイニングできます


1
なぜ誰もこの反応を考慮しないのかと私は疑問に思っています。それはシンプルで、私たちが2017年に住んでいるので、プラットフォームサポートはかなり良いです。
Rafal Pastuszak 2017

1
しかし、ダウンロード属性のiosSafariサポートはかなり赤く見えます:(
Martin Cremer

1
これは私にとってクロームでうまくいきました。firefoxの場合、アンカーをドキュメントに追加した後で機能しました。document.body.appendChild(anchor); Edgeのソリューションは見つかりませんでした...
Tompi

11
このソリューションは機能しますが、このソリューションは大きなファイルに関するUXの問題を処理しますか?300MBのファイルをダウンロードする必要がある場合、リンクをクリックしてブラウザのダウンロードマネージャに送信する前に、ダウンロードに時間がかかることがあります。fetch-progress apiを使用して独自のダウンロードプログレスUIを構築することに労力を費やすことができますが、300 MBのファイルをjs-land(メモリ内?)にロードして、単にダウンロードに渡すという疑わしい方法もあります。マネージャー。
scvnc

1
@Tompi私もEdgeとIEでこれを機能させることができませんでした
zappa

34

技術

JWTエバンジェリストとして知られているAuth0のMatias Woloskiのアドバイスに基づいて、Hawkで署名付きリクエストを生成することで解決しました。

Woloskiの引用:

これを解決する方法は、たとえばAWSのように署名付きリクエストを生成することです。

ここに、アクティベーションリンクに使用されるこの手法の例があります。

バックエンド

ダウンロードURLに署名するためのAPIを作成しました。

リクエスト:

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

応答:

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

署名付きURLを使用すると、ファイルを取得できます

リクエスト:

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

応答:

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

フロントエンド(jojoyuji作

このようにして、シングルユーザークリックですべてを実行できます。

function clickedOnDownloadButton() {

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

}

2
これはクールですが、セキュリティの観点から、OPのオプション#2(クエリ文字列パラメーターとしてトークン化)とどのように異なるのか理解できません。実際には、署名されたリクエストがより制限的である、つまり特定のエンドポイントへのアクセスのみが許可される可能性があると想像できます。しかし、OPの#2の方が簡単/手順が少ないようですが、何が問題になっていますか?
タイラーコリアー2016

4
Webサーバーによっては、完全なURLがログファイルに記録される場合があります。IT担当者がすべてのトークンにアクセスできるようにしたくない場合があります。
Ezequias Dinella 16

2
さらに、クエリ文字列を含むURLはユーザーの履歴に保存され、同じマシンの他のユーザーがURLにアクセスできるようになります。
Ezequias Dinella 16

1
最後に、これが非常に安全でないのは、URLが、サードパーティのリソースを含む、あらゆるリソースのすべてのリクエストのリファラーヘッダーで送信されることです。したがって、たとえばGoogle Analyticsを使用している場合は、GoogleにURLトークンを送信し、それらすべてに送信します。
Ezequias Dinella 16

1
このテキストはここから取られました:stackoverflow.com/questions/643355/...
Ezequias Dinella

10

すでに述べた既存の「fetch / createObjectURL」および「download-token」アプローチの代替は、新しいウィンドウをターゲットとする標準のフォームPOSTです。ブラウザがサーバーレスポンスの添付ファイルヘッダーを読み取ると、新しいタブが閉じてダウンロードが開始されます。この同じアプローチは、PDFなどのリソースを新しいタブに表示する場合にもうまく機能します。

これにより、古いブラウザーのサポートが向上し、新しいタイプのトークンを管理する必要がなくなります。また、URLでのユーザー名/パスワードのサポートがブラウザによって削除されているため、URLでの基本認証よりも長期的にサポートされます

上のクライアント側の我々は、使用target="_blank"してものSPA(単一ページのアプリケーション)のために特に重要である失敗例で回避ナビゲーションへ。

主な注意点はことである、サーバー側の JWTの検証からトークンを取得する必要がありPOSTデータヘッダからではありません。フレームワークが認証ヘッダーを使用してルートハンドラーへのアクセスを自動的に管理する場合、JWTを手動で検証して適切な認証を確実に行えるように、ハンドラーを非認証/匿名としてマークする必要がある場合があります。

フォームは動的に作成してすぐに破棄できるため、適切にクリーンアップできます(注:これはプレーンなJSで実行できますが、ここでは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();
}

非表示の入力として送信する必要がある追加のデータを追加し、それらがフォームに追加されていることを確認してください。


1
私はこの解決策が大いに支持されていると信じています。簡単で、クリーンで、完璧に機能します。
Yura Fedoriv

6

ダウンロード用のトークンを生成します。

angular内で、一時的なトークン(たとえば1時間)を取得するために認証済みリクエストを作成し、それをgetパラメータとしてURLに追加します。このようにして、好きな方法でファイルをダウンロードできます(window.open ...)


2
これは私が現在使用しているソリューションですが、かなりの作業が必要であり、「そこに」より良いソリューションがあることを望んでいるので、満足していません...
Marco Righele

3
これは利用可能な最もクリーンなソリューションだと思います。そこでは多くの作業を見ることができません。しかし、私はトークンの有効期間を短くする(例:3分)か、サーバーにトークンのリストを保持して1回限りのトークンにし、使用済みのトークンを削除します(リストにないトークンは受け入れません) )。
nabinca 2015年

5

追加のソリューション:基本認証の使用。バックエンドで少し作業が必要ですが、トークンはログに表示されないため、URL署名を実装する必要はありません。


クライアント側

URLの例は次のとおりです。

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

ダミートークンの例:

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

次に、これを押し込む<a href="...">window.open("...")、ブラウザが残りを処理します。


サーバ側

ここでの実装はユーザー次第であり、サーバーの設定に依存します- ?token=クエリパラメーターの使用とそれほど大きな違いはありません。

Laravelを使用して、簡単な方法で基本認証パスワードをJWT Authorization: Bearer <...>ヘッダーに変換し、通常の認証ミドルウェアが残りを処理できるようにしました。

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

このアプローチは有望なようですが、この方法でJWTトークンにアクセスする方法がわかりません。サーバーがこの奇妙なURLをどのように解析するか、およびjwtトークン値にアクセスする場所を教えてください。
Jiri Vetyska

1
@JiriVetyska LOL約束ですか?トークンは、ヘッダーで渡すよりも明確ですahahahha
Liquid Core
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.