原文著者:Sebastian Gingter
原文リンク:https://www.thinktecture.com/en/identityserver/prepare-your-identityserver/
翻訳者:砂漠の果ての狼
翻訳リンク:https://dotnet9.com/2022/04/How-To-Prepare-Your-IdentityServer-For-Chromes-SameSite-Cookie-Changes-And-How-To-Deal-With-Safari-Nevertheless
本稿は著者による2019年の共有であり、その中の見解や使用技術は現在の開発においても有効です。原文を読むことをお勧めします。本翻訳に関するご質問は、PRを歓迎します。
まず良いニュース:Googleは2020年2月にChrome 80をリリースする際、Googleが実装する「漸進的に優れたCookie」(Incrementally better Cookies)を含めます。これによりウェブはより安全な場所となり、ユーザーのプライバシー向上に役立ちます(サイト運営者注:現在は2022年4月28日、Chromeは複数のアップデートバージョンを既にリリースしています)。
悪いニュースは、この新しい実装がブラウザがCookieをサーバーに送信する方法に大きな変更をもたらすことです。まず、ウェブアプリケーションと認証サーバーに別々のドメインを使用している場合、Chromeのこの変更は一部のユーザーのセッション体験を壊す可能性が高いです。2つ目の問題は、変更により一部のユーザーがシステムから正しくログアウトできなくなる可能性があることです。
1. まず、このSameSiteとは何か?
Webは非常にオープンなプラットフォームです:Cookieは約20年前に設計され、2011年にRFC 6265でその設計が再検討された際、クロスサイトリクエストフォージェリ(CSRF)攻撃や過度なユーザートラッキングは大きな問題ではありませんでした。
簡単に言えば、通常のCookieの仕様では、特定のドメインに対してCookieが設定されると、そのドメインへのリクエストごとにブラウザがCookieを自動的に送信します。そのドメインに直接ナビゲートしているかどうかにかかわらず、ブラウザがそのドメインからリソース(画像など)を読み込んでいる場合、POSTリクエストを送信している場合、またはiframeにその一部を埋め込んでいる場合です。しかし、後者の可能性については、ブラウザがユーザーのセッションCookieを自動的にサーバーに送信することを望まないかもしれません。なぜなら、それはどのウェブサイトでもユーザーのコンテキストでサーバーへのリクエストを実行するJavaScriptを、ユーザーの知らないうちに実行できるからです。
これを防ぐために、SameSite Cookie仕様は2016年に起草されました。これにより、Cookieをいつ送信すべきか、または送信すべきでないかをより細かく制御できます。Cookieを設定する際に、ブラウザがリクエストにCookieを追加するタイミングをCookieごとに明示的に指定できます。そのために、ブラウザが自身のドメイン内にいる場合の同一サイトCookieと、ブラウザが異なるドメインをナビゲートしているがあなたのドメインにリクエストを送信する場合のクロスサイトCookieという概念が導入されました。
下位互換性のため、同一サイトCookieのデフォルト設定は以前の動作を変更しませんでした。この新機能はオプトインであり、Cookieに明示的に SameSite=Lax または SameSite=Strict を設定してより安全にする必要があります。これは.NET Framework(.NET Coreを含む)とすべての一般的なブラウザで実装されています。Laxは、初期ナビゲーション時にCookieをサーバーに送信するという意味で、StrictはCookieをそのドメインに既にいる場合(つまり初期ナビゲーション後の2番目のリクエスト)のみ送信するという意味です。
残念ながら、この新機能の採用は遅かったです(2019年3月のChromeのテレメトリーによると【ソース】、Chrome上で処理された全CookieのうちSameSiteフラグを使用しているのはわずか0.1%でした)。
Googleは採用を促進することを決定しました。それを強制するために、彼らは世界で最も使われているブラウザのデフォルト設定を変更することを決定しました:Chrome 80では、古いCookie処理方法を維持するためには新しい設定 SameSite=None を 必須 で指定する必要があり、SameSiteフィールドを省略すると(旧仕様が推奨したように)Cookieは SameSite=Lax を使用しているものとして扱われます。
注意: SameSite=None 設定は、Cookieが Secure としてもマークされ、HTTPS接続が必要な場合にのみ有効です。
更新: SameSite Cookieの背景について詳しく知りたい場合は、すべての詳細を含む新しい記事があります。
2. これは私に影響がありますか?もしあれば、どのように?
シングルページWebアプリケーション(SPA)があり、それが異なるドメインでホストされているIDプロバイダー(IdP、例:IdentityServer 4)に対して認証を行い、そのアプリケーションがサイレントトークンリフレッシュを使用している場合、影響を受けます。
IdPにログインすると、IdPドメインからのユーザー用のセッションCookieが設定されます。認証フローの終了時に、異なるドメインのアプリケーションはアクセストークンを受け取りますが、これらは通常短期間です。そのトークンが期限切れになると、アプリケーションはリソースサーバー(API)にアクセスできなくなります。これが毎回発生するたびにユーザーが再ログインしなければならないのは、非常に悪いユーザー体験です。
これを防ぐために、サイレントトークンリフレッシュを使用できます。この場合、アプリケーションはユーザーに見えないiframeを作成し、そのiframe内で認証プロセスを再度開始します。IdPのサイトがiframe内に読み込まれ、ブラウザがIdPに沿ってセッションCookieを送信すると、ユーザーが識別され新しいトークンが発行されます。
今やiframeはアプリケーションドメインでホストされているSPA内に存在し、そのコンテンツはIdPドメインからのものです。Cookieが明示的に SameSite=None と指定されている場合にのみ、Chrome 80はそのCookieをiframeからIdPに送信します。これはクロスサイトリクエストと見なされます。そうでない場合、サイレントトークンリフレッシュはChrome 80のリリース時に機能しなくなります。
他にも問題を引き起こす可能性のあるシナリオがあります:まず、ウェブアプリケーションやウェブサイト内に別のドメインからの要素(動画の自動再生設定など)を埋め込んでおり、それらが正常に機能するためにCookieを必要とする場合、これらの要素もSameSiteポリシーの設定が必要になります。アプリケーションがCookie認証に依存するサードパーティAPIをブラウザからリクエストする必要がある場合も同様です。
注意: 明らかに、Cookieを設定する自身のサーバーのCookie動作のみを変更できます。もしあなたの管理下にない他のドメインの要素を使用している場合、サードパーティに連絡し、問題が発生した場合はCookieの変更を依頼する必要があります。
3. では、コードを変更してSameSiteをNoneに設定します。これで大丈夫ですよね?
残念ながら、Safariには「バグ」があります。このバグにより、Safariは新しく導入された値 None をSameSite設定の有効な値として認識できません。Safariが無効な値に遭遇すると、SameSite=Strict が指定されたものとして扱い、セッションCookieをIdPに送信しません。このバグはiOS 13とmacOS 10.15 CatalinaのSafari 13で修正されましたが、macOS 10.14 MojaveとiOS 12にはバックポートされず、これらは依然として非常に大きなユーザーベースを持っています。
したがって、私たちはジレンマに陥っています:SameSiteポリシーを無視すると、Chromeユーザーはサイレントリフレッシュができなくなります。または SameSite=None を設定すると、iPhone、iPad、Macユーザーがアップデートできない、または最新のiOSおよびmacOSにアップデートできない古いデバイスでロックアウトされます。
4. 影響を受けているかどうかを確認する方法はありますか?
幸いなことに、はい。既に SameSite=None を設定している場合、おそらくiOS 12およびmacOS 10.4のSafariでアプリケーションやウェブサイトが正常に動作していないことに気付いているかもしれません。そうでない場合は、これらのバージョンのSafariでアプリケーションやウェブサイトをテストしてください。
SameSite値をまったく設定していない場合、Chromeでアプリケーションを開き、開発者ツールを開くだけです。次のような警告が表示されます:
{cookie domain} のクロスサイトリソースに関連付けられたCookieが `SameSite` 属性なしで設定されました。
Chromeの将来のリリースでは、`SameSite=None` および `Secure` で設定された場合にのみ、クロスサイトリクエストでCookieが配信されます。
詳細は開発者ツールのApplication>Storage>CookiesでCookieを確認し、
https://www.chromestatus.com/feature/5088147346030592 および
https://www.chromestatus.com/feature/5633521622188032 を参照してください。
既に SameSite=None を設定しているが Secure フラグを忘れた場合、次の警告が表示されます:
{cookie domain} のリソースに関連付けられたCookieが `SameSite=None` で設定されましたが、`Secure` なしです。
Chromeの将来のリリースでは、`SameSite=None` とマークされたCookieは `Secure` もマークされている場合にのみ配信されます。
詳細は開発者ツールのApplication>Storage>Cookiesで確認し、
https://www.chromestatus.com/feature/5633521622188032 を参照してください。
5. では、実際にどうやって解決すればいいですか?ChromeとSafariの両方で正常に動作させたいです。
私たち(同僚のBoris Wilhelmsと私)は、このトピックについていくつか調査を行い、解決策を見つけて検証しました。MicrosoftのBarry Dorransもこの問題に関する良いブログ記事を書いています。その解決策は美しくはなく、残念ながらサーバーサイドでのブラウザスニッフィングが必要ですが、シンプルな解決策であり、過去数週間でいくつかのクライアントプロジェクトで成功裏に実装しました。
この問題を解決するために、まずクロスサイトリクエストで送信する必要があるCookie(例:セッションCookie)が SameSite=None および Secure に設定されていることを確認する必要があります。プロジェクトコード内でそのCookieのオプションを見つけ、適宜調整します。これによりChromeの問題は解決し、Safariの問題が導入されます。
次に、以下のクラスとコードスニペットをプロジェクトに追加します。これにより、ASP.NET Core WebアプリケーションにCookieポリシーが追加および構成されます。このポリシーは、Cookieが SameSite=None に設定されているかどうかをチェックします。もしそうであれば、ブラウザのユーザーエージェントをチェックし、それが問題のあるブラウザ(影響を受けるSafariバージョンなど)かどうかを判断します。もしそうであれば、CookieのSameSite値を unspecified(未指定)に設定します。これにより、SameSiteの設定が完全に防止され、これらのブラウザに対して現在のデフォルト動作が再現されます。
注意: ここで提供される解決策は .NET Core 向けです。完全な .NET Framework ベースのプロジェクトの場合は、Barry Dorranの投稿で指定されているバージョンのいずれかを確認してください。
5.1 プロジェクトに追加するクラス
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Microsoft.Extensions.DependencyInjection
{
public static class SameSiteCookiesServiceCollectionExtensions
{
/// <summary>
/// -1は未指定の値を定義し、ASP.NET CoreにSameSite属性を送信しないように指示します。
/// ASP.NET Core 3.1では、<seealso cref="SameSiteMode" />列挙型にUnspecifiedの定義が追加されます。
/// </summary>
private const SameSiteMode Unspecified = (SameSiteMode) (-1);
/// <summary>
/// 未知の値をStrictとして扱うブラウザに対して、SameSite属性を適切に設定するCookieポリシーを構成します。
/// Cookieを送信する前に、<seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" />をパイプラインに追加してください!
/// </summary>
/// <remarks>
/// このコードに必要な最小限のASP.NET Coreバージョン:
/// - 2.1.14
/// - 2.2.8
/// - 3.0.1
/// - 3.1.0-preview1
/// Chromeのバージョン80(2020年2月リリース予定)以降、
/// SameSite属性がないCookieはSameSite=Laxとして扱われます。
/// Cookieを常に送信するには、SameSite=Noneに設定する必要があります。
/// しかし、現在の標準ではLaxとStrictのみが有効な値として定義されているため、
/// 一部のブラウザは無効な値をSameSite=Strictとして扱います。
/// そのため、ブラウザをチェックして、SameSite=Noneを送信するか、SameSite=Noneの送信を防ぐ必要があります。
/// 関連リンク:
/// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
/// - https://www.chromium.org/updates/same-site
/// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// - https://bugs.webkit.org/show_bug.cgi?id=198181
/// </remarks>
/// <param name="services">CookiePolicyOptionsを登録するサービスコレクション。</param>
/// <returns>変更された<see cref="IServiceCollection" />。</returns>
public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
return services;
}
private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (DisallowsSameSiteNone(userAgent))
{
options.SameSite = Unspecified;
}
}
}
/// <summary>
/// ユーザーエージェントが未知の値をStrictとして解釈することが既知かどうかをチェックします。
/// そのような場合、<see cref="CookieOptions.SameSite" />プロパティを<see cref="Unspecified" />に設定する必要があります。
/// </summary>
/// <remarks>
/// このコードはMicrosoftから引用:
/// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// </remarks>
/// <param name="userAgent">チェックするユーザーエージェント文字列。</param>
/// <returns>指定されたユーザーエージェント(ブラウザ)がSameSite=Noneを受け入れるかどうか。</returns>
private static bool DisallowsSameSiteNone(string userAgent)
{
// すべてのiOSベースのブラウザをカバーします。これには以下が含まれます:
// - iPhone、iPod Touch、iPad上のiOS 12のSafari
// - iPhone、iPod Touch、iPad上のiOS 12のWkWebview
// - iPhone、iPod Touch、iPad上のiOS 12のChrome
// これらはすべてSameSite=Noneで壊れます。なぜなら、iOSのネットワーキングスタックを使用しているからです。
// Thinktectureからの注:
// https://caniuse.com/#search=samesite によると、iOSバージョン12より下ではSameSiteをまったくサポートしていません。
// バージョン13以降、未知の値はStrictとして扱われなくなります。したがって、バージョン12のみをチェックする必要があります。
if (userAgent.Contains("CPU iPhone OS 12")
|| userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}
// Mac OS Xベースのブラウザで、Mac OSのネットワーキングスタックを使用するものをカバーします。
// これには以下が含まれます:
// - Mac OS XのSafari
// これには含まれません:
// - Mac OS XのChrome
// なぜなら、Mac OSのネットワーキングスタックを使用しないからです。
// Thinktectureからの注:
// https://caniuse.com/#search=samesite によると、MacOS Xバージョン10.14より下ではSameSiteをまったくサポートしていません。
// バージョン10.15以降、未知の値はStrictとして扱われなくなります。したがって、バージョン10.14のみをチェックする必要があります。
if (userAgent.Contains("Safari")
&& userAgent.Contains("Macintosh; Intel Mac OS X 10_14")
&& userAgent.Contains("Version/"))
{
return true;
}
// Chrome 50-69をカバーします。これらの一部のバージョンはSameSite=Noneで壊れ、
// この範囲のどのバージョンもSameSite=Noneを必要としません。
// 注:これは一部のpre-Chromium Edgeバージョンもカバーしますが、
// pre-Chromium EdgeはSameSite=Noneを必要としません。
// Thinktectureからの注:
// この仮定は検証できませんが、Microsoftの評価を信頼します。
// また、これらの古いバージョンではSameSite値を送信しないことは、
// SameSite=Noneと同じ動作になります。
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}
return false;
}
}
}
5.2 Cookieポリシーの構成と有効化
このCookieポリシーを使用するには、スタートアップコードに以下を追加する必要があります:
public void ConfigureServices(IServiceCollection services)
{
// これを追加
services.ConfigureNonBreakingSameSiteCookies();
}
public void Configure(IApplicationBuilder app)
{
// これをCookieを書き込む可能性のある他のミドルウェアの前に追加
app.UseCookiePolicy();
// これはCookieを書き込むため、Cookieポリシーの後に配置することを確認
app.UseAuthentication();
}
6. さて、これで完了ですか?
徹底的なテスト、特にChrome 79で「デフォルトCookieのSameSite」フラグを有効にした場合、およびmacOSとiOSの影響を受けるSafariバージョンでのテストを除けば、はい、これで問題ないはずです。Chrome 79でテストするには、chrome://flags に移動し、samesite を検索して SameSite by default cookies フラグを有効にします。ブラウザを再起動すると、今後の変更をすぐにテストできます。
真面目に言うと、サイレントリフレッシュ(または一般的にCookieを必要とするクロスサイトリクエスト)がこれらデバイスやブラウザで引き続き動作することを確認してください。
7. 認証サーバーのベンダーがこの問題を解決してくれるのを待つだけでいいのでは?
それはまずありません。私たちの具体的な例では、実際にCookieを管理しているのはIdentityServer自体ではありません。IdentityServerはASP.NET Coreフレームワークの組み込み認証システムに依存しており、これがセッションCookieを管理しています。ASP.NET Coreフレームワークは新しい SameSite 値 None と技術的な設定 Unspecified(SameSiteを送信しないこと)をサポートするように更新されていますが、Microsoftは ASP.NET Coreに直接ユーザーエージェントスニッフィングを導入することはできないと述べています。したがって、これは実際にあなたと既存のプロジェクト次第です。
8. まとめ
Chromeはまもなく(2020年2月)Cookieの処理に関するデフォルトの動作を変更します。将来は、SameSite が明示的に None フラグと Secure フラグで設定されている場合にのみ、Cookieを一部のクロスサイトリクエストに追加できるようになります。そうすると、一般的なSafariのバージョンでは問題が発生します。
すべてのブラウザで満足のいく動作を得るには、影響を受けるすべてのCookieを Secure および SameSite=None に設定し、その後、上記のコードのようなCookieポリシーを追加して、None を正しく解釈できないブラウザのためにSameSiteフラグを削除するオーバーライドを行います。