WebSurfer's Home

Filter by APML

URL Rewrite Module で HTTP を HTTPS へリダイレクト

by WebSurfer 13. December 2024 19:02

先日 Let's Encript を利用してHTTPS 通信ができるようにしたのを機会に、ホスティングサービスに実装されている IIS の URL Rewrite Module を使って、HTTP で要求が来た場合は 301 Moved Permanently 応答を返して HTTPS にリダイレクトするように設定しました。

ネットで検索してヒットする記事の中では Redirect HTTP to HTTPS with Windows IIS 10 が一番信頼できると思います。実際その通りやって期待通りの結果が得られます。

ですが、自分の Windows 10 PC のローカル IIS で一通りやってみました。さらに、設定がどのような意味を持つかも調べました。調べて分かったことを以下に備忘録として書いておきます。

さらに詳しく知りたい場合は、Microsoft のドキュメント「URL リライト モジュール構成リファレンス」を見ることをお勧めします。日本語版は翻訳がアレなので意味不明なところがあると思います。その場合は英語版を読んでください。

(1) URL Rewrite Module の起動

URL Rewrite Module の起動

IIS Manager を起動して書き換えを行うサイトまたはアプリケーションを選び、IIS メニューの[URL Rewrite]をクリックします。

(2) 書き換えルールの追加

書き換えルールの追加

画面右側の「操作」欄の下のメニュー[Add Role(S)...]をクリック。それにより表示された「Add Rule(s)」ダイアログの「Inbound rules」下の「Blank rule」を選択して[OK]ボタンをクリックします。

(3) Match URL セクション

Match URL セクション

上の「Edit Inbound Rule」画面が表示されます。赤枠で示したように[Name]に任意の名前を入力し、Match URL セクションの[Pattern:]に正規表現 (.*) を入力します。その他はデフォルトで上の画像の通りとなっているはずなので、そのままにしておきます。

設定の意味は、要求 URL が[Pattern:]に設定された正規表現 (.*) にマッチした時に Conditions に設定された条件を調べて、条件が満たされていれば Action の設定に従って処理を行うということです。正規表現 (.*) はどのような URL にもマッチします。

正規表現を ( ) で囲うのは意味があって、今回の例では使っていませんが、パターンをグループ化して入力の特定の部分を {R:0} とか {R:1} で取得できるからです。

[Ignore case]というのは大文字・小文字の区別をしないという意味です。チェックが入ったままにしておいてください。

(4) Conditions セクション

Conditions セクション

Conditions セクションを開いて[Add...]ボタンをクリックし、表示された「Add Condition」ダイアログで[Condition input:]と[Pattern:]に赤枠で示した通り入力します。その他はデフォルトで上の画像の通りとなっているはずなので、そのままにしておきます。

[Condition input:]に入力した {HTTPS} は、上に紹介した Microsoft のドキュメントにも書いてありますが、サーバー変数です。HTTPS 通信でない場合は OFF となります。

つまり、サーバー変数 {HTTPS} が[Pattern:]に入力した正規表現 ^OFF$ にマッチした場合は HTTPS 通信でないと判断され、Action に設定されるリダイレクトが実行されるということです。

Conditions セクションの入力・設定が完了したら[OK]ボタンをクリックします。結果は以下のようになるはずです。

設定された条件

(5) Action セクション

Action セクション

上の画像の赤枠で示した通り設定・入力します。その他はデフォルトで画像の通りとなっているはずなので、そのままにしておきます。

[Action types:]の設定[Redirect]でリダイレクトを行う応答(この記事では 301 Moved Permanently)が返されます。

[Redirect URL:]に設定した {HTTP_HOST} と {REQUEST_URI} はサーバー変数です。上に紹介した Microsoft のドキュメントにも書いてありますが、例えば要求 URL が以下の場合、

http://www.mysite.com/content/default.aspx?tabid=2&subtabid=3

取得される文字列はそれぞれ以下のようになります。

  • HTTP_HOST: www.mysite.com
  • REQUEST_URI: /content/default.aspx?tabid=2&subtabid=3

[Append query string]のチェックは外しておきます。[Redirect URL:]に設定した REQUEST_URI にクエリ文字列は含まれるので不要なはずです。

[Redirect type:]はデフォルトで[Permanent (301)]となっているはずなのでそのままにしておきます。今回のような HTTP から HTTPS へのリダイレクトでは 301 Moved Permanently が SEO 的に望ましいそうです。

(6) ルールの適用

ルールの適用

入力・設定が完了したら右側の「操作」欄の下のメニュー[適用]をクリックします。

(7) web.config

アプリの web.config に以下のコードが追加されているはずなので確認してください。

<system.webServer>
  <rewrite>
    <rules>
      <rule name="Redirect HTTP to HTTP" stopProcessing="true">
        <match url="(.*)" />
        <conditions>
          <add input="{HTTPS}" pattern="^OFF$" />
        </conditions>
        <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" 
                appendQueryString="false" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

(8) ブラウザで確認

ブラウザで確認

ブラウザのキャッシュを削除してからアドレスバーに http で始まる URL を入力して要求をかけ、上に設定したとおりリダイレクトが行われるか確認してください。

上の画像は Chrome のディベロッパーツールの Network でキャプチャした要求・応答を見たものです。設定どおり 301 Moved Permanently 応答が返ってきて、https で始まる URL にリダイレクトされています。

確認後、上の (7) の xml コードをホスティングサービスのサイトの web.config にコピーします。結果、期待通り HTTP から HTTPS へリダイレクトされることが確認できました。

Tags: , ,

Windows Server

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

by WebSurfer 25. April 2024 19:11

ASP.NET Core Web アプリを IIS でホストするためにアプリケーションプールを作成する際、設定によってはデータ保護キーがリサイクルで失われ、ブラウザから有効な認証クッキー/チケットが送られてきても復号できないので認証に失敗するという話を書きます。元の話は「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 でホストされる場合は上の 2 と 3 が関係することになります。ApplicationPoolIdentity にユーザープロファイルは無いので 2 は関係ないと思っていたのですが、そうではなかったです。以下に 2, 3 それぞれの注意点を述べます。

2. ユーザープロファイルを使用

IIS でホストされた ASP.NET Core アプリを動かすとアプリケーションプール名で c:\users フォルダにフォルダが生成され、上の 2 で言う「ユーザー プロファイルを使用できる」状態になります。

%LOCALAPPDATA% は c:\users\<アプリケーションプール名>\AppData\Local となります。(注: アプリケーションプール名の新規ユーザーが Windows に追加されるわけではなくてフォルダが生成されるだけです)

ただし、フォルダが作成されても、applicationHost.config で setProfileEnvironment 属性が true になってないとその下に ASP.NET\DataProtection-Keys ホルダは作られず、データ保護キーも生成されません。

さらに、[ユーザープロファイルの読み込み]を Ture に設定する必要があります。[ユーザープロファイルの読み込み]が False に設定されていると、データ保護キーが DataProtection-Keys フォルダに存在していても、リサイクル後は認証に失敗します。メモリに保持したデータ保護キーを使うようです。この状態で 3 の条件のレジストリにもキーがある場合はどうなるか未検証です。

上に紹介した Stackoverflow のスレッドの話では、質問者さんの Windows Server 2019 の環境では setProfileEnvironment 属性が true になっていたそうですが、自分の Windows 10 Pro 環境では false になっていました。デフォルトで true だそうですが、applicationHost.config を開いて調べた方が良さそうです。

3. レジストリを使用

アプリケーションプールを作成する際、 [.NET CLR バージョン] を [マネージド コードなし] にするとデータ保護キーはレジストリに保持されることはなく、上の 2 つ目の条件で ASP.NET\DataProtection-Keys ホルダにデータ保護キーが保持されてない限り、リサイクル後はデータ保護キー失われ、有効な認証クッキー・チケットがブラウザから送られてきても認証に失敗します。

解決策は[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしておく、もしくは、一度ホストする ASP.NET Core アプリを[.Net CLR バージョン v4.0.30319]で動かした後で、 [マネージド コードなし] に設定することです。(注: いろいろ試した結果から分かったことで Microsoft の公式ドキュメントに書いてあることではありません)

.NET CLR バージョンの設定

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

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

以上、2, 3 それぞれの注意点です。思うに、3 のレジストリの使用に頼らないで、確実に 2 のユーザープロファイルのフォルダのデータ保護キーを使用できるよう設定するのが本筋かもしれません。

なぜなら、Microsoft のドキュメント「ASP.NET Core モジュールと IIS の詳細な構成」の「IIS サイトを作成する」のセクションで、 [マネージド コードなし] に設定することが推奨されていますから。


以下にどのように検証したかを備忘録として残しておきます。環境は上にも書きましたが、自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 です。自分の Windows 10 Pro では applicationHost.config の setProfileEnvironment 属性が false になっており、以下の (8) まではその状態のままですので注意してください。

検証用のアプリは 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) の操作で入手した認証クッキーをサーバーに送信していますが、リサイクルによってデータ保護キーが失われているので復号できず例外がスローされています。

(9) 自分の Windows 10 Pro 環境では applicationHost.config の setProfileEnvironment 属性は以下の通り false になっています。(Windows Server 2019 では true になっているそうです)

<applicationPoolDefaults managedRuntimeVersion="v4.0">
  <processModel 
    identityType="ApplicationPoolIdentity" 
    loadUserProfile="true" 
    setProfileEnvironment="false" />
</applicationPoolDefaults>

なので、C:\Users\AspNetCoreCookieAuthTest\AppData\Local には ASP.NET\DataProtection-Keys ホルダは作らておれず、データ保護キーも生成されていません。

%LOCALAPPDATA%

setProfileEnvironment を ture に設定し、アプリケーションプール AspNetCoreCookieAuthTest を再起動してから、ASP.NET Core アプリを呼び出すと ASP.NET\DataProtection-Keys フォルダが作られ、その中にデータ保護キーが生成されました。

生成されたデータ保護キー

(10) その後であれば、上の (2) の画像のように[ユーザープロファイルの読み込み]が Ture に設定してあれば、データ保護キーは ASP.NET\DataProtection-Keys フォルダから取得され、リサイクル後も認証は維持されます。

(11) [ユーザープロファイルの読み込み]を False に設定して試してみましたが (Windows Server 2019 では False がデフォルトとのことです)、データ保護キーが DataProtection-Keys フォルダに存在していても、リサイクル後は認証に失敗します。メモリに保持したデータ保護キーを使うようです。

Tags: , , , , ,

CORE

ユーザー対話モード

by WebSurfer 18. September 2022 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

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar