web-dev-qa-db-ja.com

SafariがJS Fetch APIを使用してCORS Cookieを設定しない

Fetch APIを使用している場合(実際には fetch polyfill を使用)、Safariでサーバー応答からSet-Cookieを正常に適用できません。同じコードがFFとChrome(私はネイティブとポリフィルの両方を使用してテストしたfetch)で正しく機能します)。

  1. 要求はドメインを超えています。
  2. はい、credentials: trueを設定しています。
  3. サーバーはSet-Cookieヘッダーで応答します。
  4. 以降のリクエストはChromeおよびFFから送信され、Cookieリクエストヘッダーが付いていますが、Safariは送信しません。
  5. リクエストはHTTPSを使用します(証明書は自己署名され、開発ドメイン上にありますが、通常のリクエストではSafariによって受け入れられるようです)。そして

誰かが問題が何であるか知っていますか?

私はドキュメントを読み、多くの クローズされたバグレポート を調べました。私が何かを見逃していない限り、おそらく問題は 'default browser behaviour' でcookieとCORSを扱うことであり、fetchではありません(ポリフィルのソースコードを読むと、100%無知のようです)クッキー)。いくつかのバグレポートは、不正なサーバー応答がCookieの保存を妨げることを示唆しています。

私のコードは次のようになります:

function buildFetch(url, init={}) {
    let headers = Object.assign({}, init.headers || {}, {'Content-Type': 'application/json'});
    let params = Object.assign({}, init, { credentials: 'include', headers });

    return fetch(`${baseUrl}${url}`, params);
}

buildFetch('/remote/connect', {method: 'PUT', body: JSON.stringify({ code })})
.then(response => response.json())
.then(/* complete authentication */)

実際の承認リクエストは以下のとおりです。 Safariではコピー/貼り付けが難しいため、正確な要求/応答データを取得するためにcURLを使用しています。

curl 'https://mydevserver:8443/api/v1/remote/connect' \
-v \
-XPUT \
-H 'Content-Type: application/json' \
-H 'Referer: http://localhost:3002/' \
-H 'Origin: http://localhost:3002' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8' \
--data-binary '{"token":"value"}'


*   Trying 127.0.0.1...
* Connected to mydevserver (127.0.0.1) port 8443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
* Server certificate: mydevserver
> PUT /api/v1/remote/connect HTTP/1.1
> Host: mydevserver:8443
> Accept: */*
> Content-Type: application/json
> Referer: http://localhost:3002/
> Origin: http://localhost:3002
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8
> Content-Length: 15
> 
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://localhost:3002
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Api-Key, Device-Key
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
< Access-Control-Expose-Headers: Date
< Content-Type: application/json; charset=utf-8
< Content-Length: 37
< Set-Cookie: express:sess=[SESSIONKEY]=; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Set-Cookie: express:sess.sig=[SIGNATURE]; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Date: Fri, 17 Feb 2017 14:30:01 GMT
< Connection: keep-alive
< 
* Connection #0 to Host mydevserver left intact
{"some":"normal","response":"payload"}
18
Andrew

私自身の質問に答えます。

私は彼らの動機を理解していますが、これがSafariの「意図したとおりに機能する」動作であることはかなり憤慨しています。 XHR(およびネイティブに着地した場合はおそらくネイティブフェッチ)は、サードパーティのCookieの設定をまったくサポートしていません。この失敗は、スクリプトコンテキスト外のブラウザーによって処理されるため、完全に透過的であり、クライアントベースのソリューションは実際には不可能です。

ここで推奨される解決策の1つは、APIサーバーでHTMLページにウィンドウまたはiframeを開き、そこにCookieを設定することです。この時点で、サードパーティのCookieが機能し始めます。これはかなりあいまいであり、Safariがその抜け穴を閉じることがないという保証はありません。

私の解決策は、基本的に、セッションCookieが行うことを行う認証システムを再実装することです。つまり:

  1. 新しいヘッダーX-Auth: [token]を追加します。ここで、[token]は、セッションに必要な情報(理想的にはユーザーIDのみ)を含む非常に短い、存続期間の短いJWTです。アプリケーションの存続期間-ただし、セッション中にアクセス許可を変更できる場合は、アクセス許可のようなものではありません);
  2. X-AuthAccess-Control-Allow-Headersに追加します。
  3. サインイン中に、必要なペイロードを使用してセッションCookieと認証トークンを設定します(Safariユーザーと非Safariユーザーの両方がCookieと認証ヘッダーの両方を取得します)。
  4. クライアントで、X-Token応答ヘッダーを探し、それが見つかるたびにX-Token要求ヘッダーとしてエコーバックします(ローカルストレージを使用して永続性を実現できます-トークンが期限切れになるため、価値は何年もの間存続し、特定のポイントの後は償還できません);
  5. サーバーで、保護されたリソースに対するすべての要求について、Cookieを確認し、存在する場合はそれを使用します。
  6. それ以外の場合(Cookieが存在しない場合-SafariがCookieを送信しなかったため)、ヘッダートークンを探し、トークンのペイロードを確認してデコードし、提供された情報で現在のセッションを更新してから、新しい認証トークンを生成して追加します応答ヘッダー。
  7. 通常どおり続行します。

JWT(または類似のもの)は完全に異なる問題を解決することを目的としており、「リプレイ」問題のためにセッション管理に実際に使用すべきではないことに注意してください(ユーザーが独自のヘッダー状態で2つのウィンドウを開いているとどうなるかを考えてください) )。ただし、この場合は、通常必要な一時性とセキュリティが提供されます。結論としては、それらをサポートするブラウザーでCookieを使用し、セッション情報をできるだけ小さくし、JWTをできるだけ短期間で維持し、サーバーアプリをビルドして、偶発的および悪意のあるリプレイ攻撃の両方を予期する必要があります。

19
Andrew

参考までに、これを約18か月後に試したところ、このソリューションはうまくいきませんでした。または、断続的に、一部のユーザーにとっては非常に奇妙なようでした。

ここで推奨される解決策の1つは、APIサーバーでHTMLページにウィンドウまたはiframeを開き、そこにCookieを設定することです。この時点で、サードパーティのCookieが機能し始めます。これはかなりあいまいであり、Safariがその抜け穴を閉じることがないという保証はありません。

推測としては、Safariが内部で使用しているロジックは、Cookieを取得するために管理した順序、またはより複雑で不透明なものに依存すると考えられます。

React APIとは異なるホストからアプリを提供しているため、または両方のサイトを制御しているため、これが発生した場合のオプションとなる可能性があります。 DNSを使用するには:

クライアントはwww.company-name.comから提供され、APIはcompany-name.herokuapp.comにありました。 [〜#〜] cname [〜#〜]レコードapi。company-name.com-> company-name.herokuapp.comを作成し、それを使用するクライアントからAPIへのリクエストに対して同じドメインのサブドメインであるSafariは、それを「サードパーティ」のCookieと見なすのをやめました。

良い点は、コードがほとんど含まれておらず、確立されたものをすべて使用していることです...不利な点は、httpsを使用する場合、APIホストを制御または所有する必要があることです。有効な証明書が必要です。クライアントドメインの場合、またはユーザーに証明書の警告が表示されます。そのため、問題のAPIが自分のものでもパートナーのものでもない場合、これは機能しません(少なくともエンドユーザー向けのものでは機能しません)。

7
DrShaffopolis