ASP.NET Core Identity を使ったユーザー認証システムで認証チケットの有効期限が切れたとき、下の画像のようにユーザーにその旨通知する方法を書きます。
先の記事「ASP.NET Core Identity タイムアウト判定」で書いたこととほぼ同じで、違うのは先の記事ではミドルウェアを使っていたのを Login ページで行うようにし、ターゲットフレームワークを .NET Core 3.1 から .NET 8.0 に変更した点です。
Visual Studio 202 のテンプレートを使って ASP.NET Core Web アプリのプロジェクトを作る際、認証を「個別のユーザーアカウント」に設定すると ASP.NET Core Identity を利用したクッキーベースのユーザー認証が実装されます。
ユーザーがログインに成功すると、サーバーからは応答ヘッダの Set-Cookie に認証チケットを入れて認証クッキーとしてクライアント(ブラウザ)に送信します。
その後は、ブラウザは要求の都度サーバーに認証クッキーを送信するので認証状態が継続されるという仕組みになっています。
認証チケットには有効期限があります (デフォルトで 5 分、SlidingExpiration で延長あり)。一旦認証を受けたユーザーがログオフせずブラウザを立ち上げたまま長時間席を外すなどして、タイムアウトに設定した時間を超えてアクセスしなかった場合を考えてください。
タイムアウトとなった後でユーザーが席に戻ってきて再度どこかのページを要求したとすると、要求したページが匿名アクセスを許可してなければ、ログインページにリダイレクトされます。
その際、ユーザーに認証チケットが期限切れとなっていることを知らせるためには、どのようにできるかということを書きます。
一旦認証を受けたが、認証チケットが期限切れになっているというのは、Web サーバー側では以下の条件で判定できるはずです。
-
要求 HTTP ヘッダーに認証クッキーが含まれる。
-
認証クッキーの中の認証チケットが期限切れ。
一旦認証クッキーの発行を受ければ、ブラウザを閉じない限り要求の都度サーバーにクッキーを送り続けます。(下の「注 1」に書きましたが、[Remember me?]チェックボックスにチェックを入れて認証を受けた場合は話が違ってくるので注意してください)
Web サーバーが認証クッキーを取得できれば、それを復号して認証チケットを取得し、チケットの中の有効期限の日時の情報を取得できます。それで上記の 2 つの条件を確認できます。
ログインページで条件を確認して、認証チケットが期限切れになっていた場合はこの記事の一番上の画像のようにメッセージを表示してみました。
以下がログインページのコードです。スキャフォールディング機能を利用して自動生成した Login.cshtml.cs, Login.cshtml のコードに手を加えています。(スキャフォールディング方法の詳細は別の記事「ASP.NET Core MVC プロジェクトに Identity 実装」を見てください)
Login.cshtml.cs
コメントで「2024/5/11 追加」としたコードを自動生成された Login.cshtml.cs に追加しています。
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication;
// ・・・中略・・・
namespace MvcNet8App2.Areas.Identity.Pages.Account
{
public class LoginModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
// 2024/5/11 追加(下の「注 2」参照)
private readonly CookieAuthenticationOptions _options;
public LoginModel(SignInManager<ApplicationUser> signInManager,
ILogger<LoginModel> logger,
// 2024/5/11 追加(下の「注 2」参照)
IOptions<CookieAuthenticationOptions> options)
{
_signInManager = signInManager;
_logger = logger;
// 2024/5/11 追加(下の「注 2」参照)
_options = options.Value;
}
// ・・・中略・・・
public async Task OnGetAsync(string returnUrl = null)
{
// 2024/5/11 追加・・・
// .AspNetCore.Identity.Application はデフォルトのクッキー名
string cookie = Request.Cookies[".AspNetCore.Identity.Application"];
if (!string.IsNullOrEmpty(cookie))
{
IDataProtectionProvider provider =
_options.DataProtectionProvider;
// 下の「注 3」参照
IDataProtector protector = provider.CreateProtector(
// CookieAuthenticationMiddleware のフルネーム
"Microsoft.AspNetCore.Authentication.Cookies." +
"CookieAuthenticationMiddleware",
// クッキー名 .AspNetCore.Identity.Application から
// .AspNetCore. を除去した文字列
"Identity.Application",
// .NET Framework 版は "v1"、Core 版は "v2"
"v2");
// 認証クッキーから暗号化された認証チケットを復号
TicketDataFormat format = new TicketDataFormat(protector);
// 下の「注 3, 4」参照
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)
{
ViewData["AuthTicket"] = $"{userName} さん、"+
$"認証チケットの有効期限 {expiersUtc.Value} が切れています。";
}
}
// ・・・追加はここまで
// ・・・中略・・・
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
// ・・・中略・・・
}
}
}
Login.cshtml
認証ケットが期限切れの場合は cshtml から ViewData で情報をもらって期限切れのメッセージを表示するための一行を追加したのみです。
@page
@model LoginModel
@{
ViewData["Title"] = "Log in";
}
<h1>@ViewData["Title"]</h1>
@* 2024/5/11 以下の一行を追加 *@
<p><font color=red>@ViewData["AuthTicket"]</font></p>
<div class="row">
<div class="col-md-4">
<section>
・・・中略・・・
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
注 1 :
デフォルトのログインページには[Remember me?]チェックボックスがあって、これにチェックを入れて認証を受けると Set-Cookie ヘッダに expires 属性が追加され認証クッキーが HDD / SSD に保存されます。expires 属性に指定される期限は認証チケットの有効期限と同じになります。期限を過ぎるとブラウザ側で認証クッキーを削除してしまいますので、条件 1 の「要求 HTTP ヘッダーに認証クッキーが含まれる」ことの判定はできなくなります。
注 2 :
DI 機能により CookieAuthenticationOptions オブジェクトを取得します。それをベースに復号に必要な TicketDataFormat オブジェクトを取得し、認証クッキーから認証チケットを復号しています。
Visual Studio で「個別のユーザーアカウント」を認証に選んで作成したプロジェクトまたはスキャフォールディング機能を使って ASP.NET Core Identity を追加したプロジェクトでは以下のコードが Program.cs 含まれるはずです。その場合、コンストラクタの引数に IOptions<CookieAuthenticationOptions> options を含めるだけで DI 機能が働きます。
builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>()
注 3 :
IDataProtectionProvider.CreateProtector メソッドの引数ですが、これには暗号化するときに使用した値 (暗号化を特定の目的にロックするためのものらしい)と同じものを設定する必要があります。それが CookieAuthenticationMiddleware の名前、認証クッキー名、ASP.NET Identity のバージョンになるようです。
暗号化するときに使用した値と、上の CreateProtector メソッドの引数に指定したものが違うと復号に失敗し、上のコードで authTicket が null になり、次の行の authTicket.Principal で NullReferenceException がスローされます。
注 4 :
別の記事「ASP.NET Core でのデータ保護キーの管理」で書きましたがデータ保護キーがリサイクルで失われることがあります。その場合は復号に失敗し、上のコードで authTicket が null になり、次の行の authTicket.Principal で NullReferenceException がスローされます。
リサイクルで失われるようなことは無くても、データ保護キーの有効期限はデフォルトで 90 日だそうですので、有効期限を過ぎると復号に失敗すると思われます (未検証・未確認です)。