WebSurfer's Home

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

ASP.NET Core Identity タイムアウト判定

by WebSurfer 2020年10月31日 18:18

先の記事「ASP.NET Identity タイムアウト判定」の Core 3.1 版です。(先の記事は .NET Framework 版です。Core 版とはかなりの部分で異なります)

有効期限切れの通知

ASP.NET Identity の認証システムは、基本的に旧来のフォーム認証と同様で、ログイン後はクッキーに認証チケットを入れて要求毎にサーバーに送信するので認証状態が維持されるという仕組みになっています。

一旦認証を受けたユーザーがログオフせずブラウザを立ち上げたままタイムアウトとなって、再度どこかのページを要求したとすると、要求したページが匿名アクセスを許可してなければ、ログインページにリダイレクトされます。

その時、認証チケットが期限切れになった(認証されているか否かではありません)というのを、どのようすれば判定できるかということを書きます。

Web サーバー側では以下の条件で判定できるはずです。

  • 要求 HTTP ヘッダーに認証クッキーが含まれる。
  • 認証クッキーの中の認証チケットが期限切れ。

Web サーバーが認証クッキーを取得できれば、それを復号して認証チケットを取得し、チケットの中の有効期限の日時の情報を取得できます。それで上記の 2 つの条件を確認できます。

認証クッキーを復号する方法ですが、旧来のフォーム認証の場合とはもちろん、.NET Framework 版の ASP.NET Identity の場合ともかなり異なっています。

そのあたりはググって調べた stackoverflow の記事 How to decrypt .AspNetCore.Identity.Application cookie in ASP.NET Core 3.0?How to manually decrypt an ASP.NET Core Authentication cookie? の回答を参考にさせていただきました。

それを先の記事と同様に HTTP モジュールとして実装してみようと思いましたが、Core には HTTP モジュールも HTTP ハンドラも web.config もないとのこと。

.NET Framework の ASP.NET の HTTP モジュールに代わるものが ASP.NET Core ではミドルウエアになるらしいです。なので、以下の Microsoft のドキュメントを参考に、ミドルウェアを作ってみました。

以下がそのミドルウェアのコードです。上の画像がこのミドルウェアにより認証チケットの期限切れをチェックして Login 画面上でユーザーに通知したものです。

ただし、ユーザーへの通知を下のコードの最後の方にあるように HttpResponse.WriteAsync を使って書き込んだ場合、見かけは上の画像のようになりますが、実は <html></html> タグの外(この例では最後)に書き込まれています。また、上の記事「ASP.NET Core のミドルウェア」の警告に書いてありますが、next.Invoke の前後で応答本文に書き込むというのはそもそもやってはいけないことのようです。

今回はミドルウェアに実装してしまいましたが、ユーザーへの通知を行うならミドルウェアでなく Login ページに下のコードを実装して期限切れを判定し、所定の位置に書き込むようにする方がよさそうです。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using System.Security.Principal;

namespace MvcCoreApp.Middleware
{
    public class AuthExpireCheckMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly CookieAuthenticationOptions _options;

        // コンストラクタの引数を以下のように設定しておくことにより
        // CookieAuthenticationOptions を DI 機能により取得できる
        public AuthExpireCheckMiddleware(
            RequestDelegate next,
            IOptions<CookieAuthenticationOptions> options)
        {
            _next = next;
            _options = options.Value;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // ここでの処理は必要ないので即後続のミドルウェアに処理を渡す。
            // 応答がクライアントに送信された後に _next.Invoke を呼び出すと
            // 例外がスローされるそうなので注意(ここで Response.WriteAsync
            // は書けない)

            await _next.Invoke(context);

            // 認証チケットの期限が切れた後、認証が必要なページにアクセス
            // して Login ページにリダイレクトされた時のみ対応。
            // "/Identity/Account/Login" は Visual Studio のテンプレートで
            // プロジェクトを作った際のデフォルト。必要あれば変更可
            if (context.Request.Path.ToString().
                                     Contains("/Identity/Account/Login"))
            {
                // 認証クッキーが送られてくる時のみ対応。クッキー名
                // .AspNetCore.Identity.Application はデフォルト。
                // 必要あれば変更可
                string cookie 
                    = context.Request.Cookies[".AspNetCore.Identity.Application"];
                if (!string.IsNullOrEmpty(cookie))
                {
                    IDataProtectionProvider provider = 
                                        _options.DataProtectionProvider;

                    // 下の「注2」参照
                    IDataProtector protector = provider.CreateProtector(
                        // CookieAuthenticationMiddleware のフルネームらしい
                        "Microsoft.AspNetCore.Authentication.Cookies." +
                        "CookieAuthenticationMiddleware",
                        // クッキー名から .AspNetCore. を削除した文字列
                        "Identity.Application",
                        // .NET Framework 版は "v1"
                        "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)
                    {    
                        // ユーザーに通知
                        // 以下のコードでは <html></html> 外に書き込まれるので注意
                        await context.Response.WriteAsync("<h3><font color=red>" +
                            userName +
                            " さん、認証チケットの有効期限が切れています" +
                            "</font></h3>");
                    }
                }
            }
        }
    }
}

注1

上のコードのように DI 機能により CookieAuthenticationOptions オブジェクトを取得できます。それをベースに復号に必要な TicketDataFormat オブジェクトを取得し、認証クッキーから認証チケットを復号しています。

Visual Studio のテンプレートを使って「個別のユーザーアカウント」を認証に選んで作成したプロジェクトでは以下のコードが Startup.cs 含まれています。その場合、コンストラクタの引数に IOptions<CookieAuthenticationOptions> options を含めるだけで DI 機能が働きます。コントローラーのコンストラクタでも同じです。

services.AddDefaultIdentity<IdentityUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

上の設定が Startup.cs に含まれない場合、上に紹介した stackoverflow の記事 How to decrypt .AspNetCore.Identity.Application cookie in ASP.NET Core 3.0? にあるようなサービスの登録が必要です、

注2

IDataProtectionProvider.CreateProtector の引数ですが、これは .NET Framework の MachineKey.Protect / MachineKey.Unprotect の第 2 引数と同様に暗号化テキストを特定の目的にロックするためのもののようです。

上の紹介した stackoverflow の記事に少し記述があります。Microsoft のドキュメントでは「ASP.NET アプリ間での認証の共有」に記載があるのを見つけました。

注3

上に書いたミドルウェアが動くようにするには、Startup.cs の Configure メソッドでの登録が必要です。その具体例は以下の通りです。場所は既存の app.UseEndpoints の直前で良いです。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

    // ・・・中略・・・

    // これを追記
    app.UseMiddleware<MvcCoreApp.Middleware.AuthExpireCheckMiddleware>();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");

        endpoints.MapRazorPages();
    });
}

注4

上にも書きましたが、next.Invoke の前後で応答本文に書き込むというのはそもそもやってはいけないことのようですし(特に next.Invoke の前はエラーになるとのこと)、 <html></html> タグの外に書き込まれてしまうので、ユーザーへの通知を行うならミドルウェアではなく Login ページ内で行うべきと思います。

認証に「個別のユーザーアカウント」を選んでプロジェクトを作成すると Login, Register 他の認証関係の機能は Razor Class Library (RCL) として提供され、ソースコードはプロジェクトには含まれないというところが問題ですが、スキャフォールディング機能を利用して Login ページをオーバーライドすることで対応できそうです。

Tags: , , ,

Authentication

開発環境で Kestrel 利用 (CORE)

by WebSurfer 2020年9月25日 10:55

先の記事「ASP.NET Core アプリの Web サーバー」に書きましたが、Visual Studio 2019 から ASP.NET Core アプリを実行するとデフォルトでは IIS Express を使用するインプロセスホスティングモデルになります。(注: 2021/11/9 にリリースされた Visual Studio 2022 では下の画像のツールバーにあるドロップダウンはデフォルトで「プロジェクト名」が選択されていました。なので、そのまま実行するとアプリは Kestrel で実行されます)

インプロセス ホスティング モデル

それを以下のように Kestrel をエッジサーバーとして使うようにするにはどうすれば良いかということを書きます。

Kestrel

実は最近まで知らなかったのですが、Visual Studio のツールバーにあるドロップダウンの選択で容易に変更できるそうです。

ドロップダウンの選択

Visual Studio 2019 の場合デフォルトでは IIS Express が選択されています。その状態で Visual Studio 2019 から[デバッグ(D)]⇒[デバッグの開始(S)](または[デバッグなしで開始(H)]) でアプリを起動するとIIS Express を使用するインプロセスホスティングモデルで動きます。

ドロップダウンの選択をプロジェクト名(上の画像の例では MvcCoreApp)に変更して Visual Studio からアプリを起動すると以下のように dotnet run コマンドによってアプリが Kestrel で実行され、

dotnet run コマンド

処理された応答が自動的にブラウザに表示されます(自動的にブラウザに表示されるのは Visual Studio を使った場合です。下の注記を見てください)。

Chrome で表示

(注: dotnet run コマンドはアプリを実行するだけです。ブラウザに表示するにはブラウザを立ち上げてアドレスバーに dotnet run コマンドで表示される Now listenung on: にある URL(上の画像では https://localhost:5001 または http://localhost:5000)を入力して Kestrel に要求をかける必要があります。Visual Studio はブラウザの立ち上げと表示まで自動的に実行してくれます。さらに、HTTPS 通信に使う開発用のサーバー証明書も自動的に発行してくれます)

ドロップダウンの選択の変更によって何がどう変わるかの詳しい説明は Microsoft のドキュメント「ASP.NET Core で複数の環境を使用する」の「開発と launchSettings.json」のセクションに書いてあります。

簡単に言うと、Visual Studio のドロップダウンの選択によって launchSettings.json の profiles を IIS Express かプロジェクト名(上の画像の例では MvcCoreApp)のどちらかに切り替えることができ、それぞれの "commandName" キーの設定に応じて IIS Express を使用するか Kestrel を使用するかを指定できるそうです。

ちなみに、"commandName" キーに設定できるのは IISExpress, IIS, Project のいずれかということです。

なお、launchSettings.json が使えるのはローカルの開発マシンだけです。運用環境にデプロイされることはありません。なので、上記は開発環境だけでの話ですので注意してください。

開発環境でアウトプロセスホスティング構成(Kestrel を Web サーバー、IIS Express をリバースプロキシとして使う)とするのはどうすればいいかは不明です。Microsoft のドキュメント「ASP.NET Core モジュール」にプロジェクトファイルで AspNetCoreHostingModel プロパティの値を OutOfProcess に設定すると書いてあるのを見つけましたが未検証・未確認です。今後の検討課題ということで・・・

Tags: ,

CORE

環境別の appsettings.json (CORE)

by WebSurfer 2020年9月23日 12:10

開発環境と運用環境では、通常、接続文字列やエラー表示などが異なります。.NET Framework 版の ASP.NET Web アプリではそれらの設定は web.config ファイルに含まれており、運用環境にデプロイする際は該当部分を書き換える必要があります。

Visual Studio で開発を行う場合は、運用環境にデプロイする際書き換える部分をあらかじめ Web.Release.config というファイルに作っておき、Visual Studio の発行ツールを使ってデプロイ時に書き換えるということが可能です。

Web.Release.config

上の画像は Visual Studio Community 2019 のテンプレートを使って作成した ASP.NET Web Forms アプリで自動生成される Web.Release.config です。接続文字列とエラー表示の設定を書き換えるためのひな型が含まれています。

しかし、Core 版には .NET Framework 版と同じ機能はなさそうです。では、どうするかですが、以下の Microsoft のドキュメントに説明がありました(日本語版は翻訳がダメなので意味が分からないときは英語版を見てください)。

ASP.NET Core の構成

ASP.NET Core で複数の環境を使用する

appsettings.json に加えて、必要に応じて appsettings.Development.json, appsettings.Staging.json, appsettings.Production.json を追加し、それにそれぞれの環境専用の接続文字列などの設定を書きます。(注: Visual Studio のテンプレートを使うと appsettings.json と appsettings.Development.json は自動的に作成されプロジェクトに含まれているはずです)

appsettings.json

そして、例えば接続文字列を環境によって変えたい場合、appsettings.Development.json, appsettings.Staging.json, appsettings.Production.json にそれぞれの環境の接続文字列を書けば、環境変数 ASPNETCORE_ENVIRONMENT の値 (Development, Staging, Production のいずれか)によってファイルを選んでそれから接続文字列を取得してきます。

結果は appsettings.json の設定プラス appsettings.環境.json の設定になります。appsettings.json の設定を丸ごと appsettings.環境.json の設定に置き換えるということはしませが、設定がダブった場合(同じ名前のキーがあった場合)は、上に紹介した記事「ASP.NET Core の構成」に書いてある通り、appsettings.json の方をオーバーライドします。

例えば、開発環境においては、appsettings.Development.json 構成が appsettings.json の値を上書きします。運用環境では、appsettings.Production.json 構成が appsettings.json の値を上書きします。

なお、環境変数 ASPNETCORE_ENVIRONMENT が設定されてない場合は、上に紹介した記事「ASP.NET Core で複数の環境を使用する」に書いてあるように、デフォルトで Production になることに注意してください。

では、環境変数 ASPNETCORE_ENVIRONMENT をどのように設定するかですが・・・

開発環境では Visual Studio のテンプレートを使ってプロジェクトを作ると自動的に生成される launchSettings.json ファイルに "Development" と設定されています。

launchSettings.json

(注: 上の画像で赤枠で囲った方は Visual Studio から実行した場合デフォルトになる IIS Express を使ってインプロセスホ��ティングで実行する場合の設定です。下の方は Kestrel で実行する場合の設定です)

"Development" を "Staging", "Production" に変更して試してみると、上に書いた通り、取得してくる先の appsettings.環境.json が異なるのが分かるはずです。

運用環境では launchSettings.json ファイルは使いません。デプロイもされません。ではどうするかですが、そもそも環境変数なので、上に紹介した記事「ASP.NET Core で複数の環境を使用する」の「環境を設定する」のセクションに書いてあるようにして設定できるそうです。

または、ASPNETCORE_ENVIRONMENT が設定されてなければデフォルトで Production なので何も設定しないということでも良いかもしれません。

上に書いた appsettings.json の設定で接続文字列は環境によって書き換えできますが、エラー表示の設定の変更はできないので注意してください。それは Startup.cs の Configure メソッドで行います(HTTP 要求を処理するパイプラインを構成するためのミドルウェアの一部として登録)。

Visual Studio のテンプレートを使ってプロジェクトを作成すると、Startup.cs ファイルに以下のような Configure メソッドが自動的に生成されるはずです(日本語のコメントは自分が追加したものです)。

Startup.cs

環境変数 ASPNETCORE_ENVIRONMENT が Development に設定されていると env.IsDevelopment() が true になって、開発用の詳細エラーページが表示されるようになります。

Tags: , , , ,

CORE

About this blog

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

Calendar

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

View posts in large calendar