WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Core でのデータ保護キーの管理

by WebSurfer 2024年4月25日 19:11

ASP.NET Core Web アプリを IIS でホストするためにアプリケーションプールを作成する際、以下の画像のように[.NET CLR バージョン]を[マネージド コードなし]にするとデータ保護キーがリサイクルで失われ、ブラウザから有効な認証クッキー/チケットが送られてきても復号できないので認証に失敗するという話を書きます。(元の話は「ASP.NET COREで、Cookie認証が維持できない」です)

アプリケーションプールの追加

ASP.NET Core アプリのデータ保護キーの管理は、Microsoft のドキュメント「ASP.NET Core でのデータ保護のキー管理と有効期間」によると、アプリにより運用環境が検出され、以下のいずれかになるそうです。

  1. アプリが Azure Apps でホストされている場合、キーは %HOME%ASP.NET_DataProtection-Keys フォルダーに保持されます。
  2. ユーザー プロファイルを使用できる場合、キーは %LOCALAPPDATA%\ASP.NET\DataProtection-Keys フォルダーに保持されます。
  3. アプリが IIS でホストされている場合、HKLM レジストリ内の、ワーカー プロセス アカウントにのみ ACL が設定されている特別なレジストリ キーにキーが保持されます。
  4. これらの条件のいずれにも該当しない場合、キーは現在のプロセスの外部には保持されません。 プロセスがシャットダウンすると、生成されたキーはすべて失われます。

(ご参考までに、.NET Framework 版の ASP.NET Web アプリの場合は web.config に配置する machineKey 要素 (ASP.NET 設定スキーマ) に従ったデータ保護キーの管理を行い、デフォルト AutoGenerate, IsolateApps の場合はレジストリにデータ保護キーが保持されます)

ということで、IIS でホストされる場合は上の 3 つ目に該当することになり、データ保護キーはレジストリに保存されリサイクルで失われことはない・・・はずですが、上の画像のように[.NET CLR バージョン]を[マネージド コードなし]に設定してアプリケーションプールを作成するとデータ保護キーはレジストリに保存されないようでリサイクルで失われます。なので、リサイクル後はブラウザから有効な認証クッキー/チケットが送られてきても認証に失敗します。(自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 での検証結果です。Windows Server の IIS では少し様子が違うという話がありますが、リサイクルでキーが失われる問題はあるそうです)

知ってました? 自分は知らなかったです。(汗) ASP.NET Core Identity のデフォルトの認証チケットの有効期限は 5 分ですし、短い方がセキュリティ的に望ましいし、短くしていると気が付かないかもしれません。(言い訳)

Microsoft のドキュメント「ASP.NET Core モジュールと IIS の詳細な構成」の「IIS サイトを作成する」のセクションに、

"6. [アプリケーション プールの編集] ウィンドウで、 [.NET CLR バージョン] を [マネージド コードなし] に設定します。"

と書いてあり、 [マネージド コードなし] に設定するのは必須ではないものの推奨されています。ということは、Microsoft の推奨に従ってアプリケーションプールを設定すると、リサイクルで認証が維持できないということになります。

解決策は、アプリケーションプールの追加を行う際、下の画像のように[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしておくことです。(注: いろいろ試した結果から分かったことで Microsoft の公式ドキュメントに書いてあることではありません)

.NET CLR バージョンの設定

Micosoft の推奨に従って [.NET CLR バージョン] を [マネージド コードなし] に設定したいということであれば、設定を[.Net CLR バージョン v4.0.30319]のままにしておいて一度ホストする ASP.NET Core アプリを動かした後で、 [マネージド コードなし] に設定します。

とにかく一度[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしてアプリを動かせば、レジストリにデータ保護キーが保持されるようなって、リサイクルでデータ保護キーが失われることはなくなるようです。理由は不明です。

バグっぽいのでそのうち修正されるかもしれませんが、修正されたことがはっきりするまでは要注意だと思います。

なお、アプリケーションプールの名前はデータ保護キーの保存先のレジストリと関連付けられるようですので注意してください。そして、そのレジストリはアプリケーションプールを削除しても残るようです。レジストリにデータ保護キーを保存できるように設定したアプリケーションプールを一旦削除してから、同じ名前で [.NET CLR バージョン] を [マネージド コードなし] にしてアプリケーションプールを作成しても、リサイクル後の認証は維持できます。

また、上に紹介した Stckoverflow の質問者さんの環境では、[ユーザープロファイルの読み込み]の設定が True / False で違いがあって、True の場合は問題ないとのことです。自分が試した限りでは True でも False でも関係なくダメで、リサイクル後の認証は維持できませんでした。想像ですが、上に書いた 2 つ目の条件の「ユーザー プロファイルを使用できる場合」に該当していたのではないかと思われます。

以下にどのように検証したかを備忘録として残しておきます。環境は上にも書きましたが、自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 です。アプリは Microsoft のドキュメント「ASP.NET Core Identity を使用せずに cookie 認証を使用する」から入手した .NET 6.0 の Razor Pages アプリに少し手を加えたものです。

(1) C:\WebSites2019 というフォルダ下に AspNetCoreCookieAuth2 という名前のフォルダを作成。

(2) IIS Manager を起動して AspNetCoreCookieAuthTest という名前でアプリケーションプールを新たに作成。その際[アプリケーション プールの編集]ウィンドウで[.NET CLR バージョン]を[マネージド コードなし]に設定。その他はデフォルトのままとしておきます。結果は以下の画像の通りです。

アプリケーションプール詳細

(3) IIS Manager のサイトの追加で、上記 (1) のフォルダをサイトに設定。サイト名は AspNetCoreCookieAuth2 とし、アプリケーションプールは上記 (2) で新たに作成したものを設定。

サイトの設定

サイトバインド設定は、種類: http, IP アドレス: 未使用の IP アドレスすべて, ポート: 80, ホスト名: www.aspnetcorecookieauth2.com とします。

サイトバインド

(4) hosts ファイルに 127.0.0.1 www.aspnetcorecookieauth2.com を追加します。

(5) ダウンロードした .NET 6.0 のサンプル Razor Pages プロジェクトを Visual Studio 2022 で開��て以下のように手を加えます。

  • 永続化(認証クッキーを HDD/SSD に保存)するため既存の Login.cshtml.cs の IsPersistent = true, のコメントアウトを解除
  • 認証チケットの有効期限を 1 週間に設定するため Program.cs の .AddCookie のオプションで有効期限を指定
  • 暗号化されている認証チケットを復号し、認証チケットの有効期限の日時を取得するページ AuthExpireCheck を追加。cshtml.cs のコードは以下の通りです(cshtml のコードは省略します)
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;

namespace CookieSample.Pages
{
    public class AuthExpireCheckModel : PageModel
    {
        private readonly CookieAuthenticationOptions _options;

        public AuthExpireCheckModel(IOptions<CookieAuthenticationOptions> options)
        {
            _options = options.Value;
        }

        public string Message { get; set; }

        public void OnGet()
        {
            try
            {
                // デフォルトのクッキー名は ASP.NET Core Identity の
                // .AspNetCore.Identity.Application とは異なり
                // .AspNetCore.Cookies となっているので注意
                string cookie = Request.Cookies[".AspNetCore.Cookies"];
                if (!string.IsNullOrEmpty(cookie))
                {
                    IDataProtectionProvider provider = _options.DataProtectionProvider;
                    IDataProtector protector = provider.CreateProtector(
                        // CookieAuthenticationMiddleware のフルネーム
                        "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                        // クッキー名 .AspNetCore.Cookies から .AspNetCore.
                        // を削除した文字列
                        "Cookies",
                        // .NET Framework 版は "v1"、Core 版は "v2"
                        "v2");

                    // 認証クッキーから暗号化された認証チケットを復号
                    TicketDataFormat format = new TicketDataFormat(protector);
                    AuthenticationTicket authTicket = format.Unprotect(cookie);

                    // ユーザー名を取得
                    ClaimsPrincipal principal = authTicket.Principal;
                    IIdentity identity = principal.Identity;
                    string userName = identity.Name;

                    // 認証チケットの有効期限の日時を取得
                    AuthenticationProperties property = authTicket.Properties;
                    DateTimeOffset? expiersUtc = property.ExpiresUtc;

                    // ExpiresUtc と現在の時刻を比較して期限切れか否かを判定
                    if (expiersUtc.Value < DateTimeOffset.UtcNow)
                    {
                        Message = $"{userName} 認証チケット期限切れ {expiersUtc.Value}";
                    }
                    else
                    {
                        Message = $"{userName} 認証チケット期限内 {expiersUtc.Value}";
                    }
                }
                else
                {
                    Message = "認証クッキーがサーバーに送信されてきていません。";
                }
            }
            catch (CryptographicException ex)
            {
                Message = $"CryptographicException: {ex.Message}";
            }
            catch (Exception ex)
            {
                Message = $"Exception: {ex.Message}";
            }
        }
    }
}

(6) Microsoft のチュートリアル「IIS に ASP.NET Core アプリを発行する」の「アプリを発行および配置する」セクションに従って、上のステップで作成した C:\WebSites2019\AspNetCoreCookieAuth2 フォルダにプロジェクトを発行。インプロセスホスティングになります (Microsoft 推奨)。

インプロセスホスティング

(7) ブラウザからサイトにアクセスしてログインしてから上の (5) で追加した AuthExpireCheck ページを要求すると以下の通りとなります。一旦ブラウザを閉じて、再度立ち上げ AuthExpireCheck ページを要求しても認証は維持されます。(IsPersistent = true としたことによる Set-Cookie の expires 属性は有効に機能していることが確認できる)

認証は維持されている

(8) IIS Manager を操作してアプリケーションプルをリサイクルし、ブラウザから AuthExpireCheck ページを要求すると以下の通りとなります。ブラウザからは上の (7) の操作で入手した認証クッキーをサーバーに送信していますが、リサイクルによってデータ保護キーが失われているのでユーザー認証に失敗しています。

認証されてない

Tags: , , , , ,

CORE

ユーザー対話モード

by WebSurfer 2022年9月18日 13:58

IIS でホストされる ASP.NET Web アプリはユーザー対話モードでは動かないという話を書きます。

Environment.UserInteractive

ASP.NET Web アプリを、開発環境で Visual Studio から実行して IIS Express 上で動かすと期待通り動くが、運用環境で IIS にデプロイすると動かないという話を耳にします。

その原因のほどんどは、(1) ワーカープロセスのアクセス権の違い、(2) プロセスがユーザー対話モードで動いているか否かです。(加えて、Session 0 分離による制約もあります。詳しくは、Microsoft の文書 Impact of Session 0 Isolation on Services and Drivers in Windows を見てください)

開発環境の時は、PC にログインしたアカウントで Visual Studio を起動しているので、(1) は PC にログインしたユーザーアカウントの権限、(2) はユーザー対話モードになっています。

運用環境で IIS にデプロイした時は、(1) はデフォルトでは Network Service または AppPoolIdentity という権限が低いアカウント、(2) は非ユーザー対話モードです。

ユーザー対話モードにならないということは、ユーザーが対話するためのグラフィカルユーザーインターフェイスが存在しないということで、モーダルダイアログやメッセージボックスは表示できません。

ワーカープロセスのアカウントは WindowsIdentity.GetCurrent メソッドで、ユーザー対話モードで実行されているか否かは Environment.UserInteractive プロパティで調べることができます。上の画像の赤枠部分が Windows 10 の IIS 10 でホストされる ASP.NET Web Forms アプリで調べた結果です。

上記 (1) の権限の問題は、Network Service または AppPoolIdentity に必要な権限を与えるとか、権限を持つアカウントを偽装するとかで解決します。

または、以下の画像のように IIS Manager を操作して、プロセスモデルの ID に必要な権限を持つユーザーアカウントを設定することでも解決できます。

ワーカープロセスのアカウント変更

しかし、(2) のユーザー対話モードについてはそれでは何ともなりません。ユーザー対話モードにする手段は少なくとも自分が調べた限りでは無かったです。

実は、上の画像のようにプロセスモデルの ID に管理者権限を持つユーザーアカウントを設定すれば Environment.UserInteractive が True(=ユーザー対話モード)になると思い違いしていました。

昔の Teratail のスレッド「WebBrowserの動作に影響するPC環境の相違点」に回答する際調べたことなのですが、すっかり忘れていたので備忘録として残しておくことにした次第です。

Tags: , ,

ASP.NET

要求の中断による処理のキャンセル (MVC5)

by WebSurfer 2021年7月12日 10:30

.NET Framework 版の ASP.NET Web アプリで、クライアントによる要求の中断を検出してサーバー側の処理をキャンセルする話を書きます。(ASP.NET Core の場合は先の記事「要求の中断による処理のキャンセル (CORE)」を見てください)

要求の中断

クライアントによる要求の中断とは、上の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

ASP.NET 4.5 以降には HttpResponse.ClientDisconnectedToken プロパティが追加されています。このプロパティで取得できる CancellationToken によって、クライアントが要求の中断操作を行った場合に操作を取り消す通知を配信できます。(ASP.NET Core の HttpContext.RequestAborted プロパティと同様)

ただし、ASP.NET 4.5 以降という条件以外にも、上に紹介した Microsoft のドキュメントに書いてあるように IIS 7.5 以降の統合モードでのみサポートされているということですので注意してください

Visual Studio 2019 を使って、以下のコードを [デバッグ(D)] ⇒ [デバッグの開始(S)] で IIS 10 Express で実行して検証してみました。検証に使用したブラウザは Edge v91.0.864.67, Chrome v91.0.4472.124, Firefox v89.0.2, IE11, Opera v77.0.4054.203 で、いずれもクライアントによる要求の中断によってサーバー側での処理の中断が確認できました。

using System;
using System.Web.Mvc;
using System.Threading.Tasks;
using System.Diagnostics;

namespace Mvc5App2.Controllers
{
    public class HomeController : Controller
    {

        // ・・・中略・・・

        public async Task<ActionResult> Cancel()
        {
            var token = Response.ClientDisconnectedToken;

            Debug.WriteLine($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            Debug.WriteLine($"end: {DateTime.Now:ss.fff}");
            return View();
        }
    }
}

ASP.NET Core MVC と違ってアクションメソッドの引数に CancellationToken を追加しても ClientDisconnectedToken プロパティで取得される CancellationToken はバインドされないので注意してください。なので、上のコードのようにする必要があります。(引数に追加すると CancellationToken がバインドされますが、それは ClientDisconnectedToken プロパティで取得できるものとは別物のようで要求の中断による通知は出ません)

上のコードの Task.Delay(5000, token) では渡した token を Deley メソッドの中で継続的に観察しているようで、このメソッドが実行が開始された後でもそれから 5 秒以内ならブラウザで要求を中断すると TaskCanceledException がスローされて処理が終わります。

ただし、Entity Framework を使ってデータベースにアクセスして処置を行うときに使う ToListAsync とか SaveChangesAsync などや、HttpClient の SendAsync とか PostAsync なども同様かどうかは調べてなくて分かりません。今後の検討課題ということで・・・

Tags: , , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar