先ほど述べた UserAuthentication() と UserAuthorization() の違いは次の通りです。前者はログインしている人物を確認するためのものであり、後者はログインしている人物が何を実行できるかを決定します。
例えば、従業員が社内システムにログインする際、アカウント(従業員ID、名前、メールアドレスなど)とパスワードを入力する必要があり、システムは誰がログインしたかを認識します。これが Authentication(認証) です。Authentication を処理する方式には Cookie、Token、サードパーティ認証(OAuth や API-token)、OpenId、SAML などがあります。
従業員がシステムにログインした後、一般従業員には通常、部門をまたいだ権限や管理権限はありません。自分自身または所属部門の情報しか見ることができません。例えば、生産部門の従業員は経理部門の財務情報を見ることができませんが、経理部門がコスト計算のために生産部門の原材料価格を見ることができるのは Authorization(認可) です。
ログインしている人物が何を実行できるかを決定する前に、まずログインしている人物が誰かを知る必要があるため、UserAuthentication() は UserAuthorization() の前に配置しなければなりません。
ASP.NET Core Identity は Claim ベースの認証を使用しています。Claim を理解するためには、Claim、ClaimsIdentity、ClaimsPrincipal が何かを説明する必要があります。
Claim とはユーザーに関する情報であり、Claim Type と Claim Value(省略可能)で構成されます。Claim には 名前、電話番号、役割、メールアドレス、さらには 役割 などが含まれます。Authorization ではこの Claim を使ってユーザーに権限があるかどうかを判断します。
new System.Security.Claims.Claim(ClaimTypes.Role, role.Name)
ClaimsIdentity は複数の Claim の集合です。例えば、運転免許証 には 名前、生年月日、電話番号 が記録されており、運転免許証 自体が ClaimsIdentity です。
ClaimsPrincipal は複数の ClaimsIdentity の集合です。台湾人の場合、身分証、健康保険証、運転免許証などを持っており、身分証、健康保険証、運転免許証はそれぞれ ClaimsIdentity であり、それらを保持する人自身が ClaimsPrincipal です。
各 HTTP request は HttpContext オブジェクトを生成し、そのオブジェクトには現在の request の情報が格納されています。その中に ClaimsPrincipal 型の User というプロパティがあり、このプロパティは UseAuthentication() によって導入される Authentication Middleware によって生成されます。
ログイン機構は Cookie または JWT で実装できますが、Authentication Middleware はどの方式を使って User プロパティを生成するかをどうやって判断するのでしょうか。その答えは Authentication Scheme と Authentication Handlers にあります。
Authentication Handlers
Authentication Handlers は認証を処理する方法です。ASP.NET Core Identity では AuthenticateAsync() API を呼び出してユーザーがログイン済みかどうかを検証し、検証に失敗した場合は ChallengeAsync() を呼び出してユーザーを ログインページ にリダイレクトし、認可に失敗した場合は ForbidAsync() を使ってユーザーのアクセスを禁止します。もちろん、これらの動作を自分で実装することも可能です。次の例では JWT と Cookie の認証方式を使用しています。前者を使用する場合は JWT token を検証し、ClaimsPrincipal を生成して HttpContext.User に設定します。後者を使用する場合は現在の request の cookie を確認し、ClaimsPrincipal を生成します。
builder.Services.AddAuthentication()
.AddJwtBearer()
.AddCookie();
Authentication Scheme
いずれかの方式で Authentication Handlers を登録することを Authentication Scheme と呼びます。各 Authentication Scheme には一意の名前が付けられ、Authentication Handlers を独自に設定することもできます。以下のコードの結果は上記と同じになります。どちらも既定の Scheme 名を持っているためです。
builder.Services.AddAuthentication()
.AddJwtBearer("Bearer")
.AddCookie("Cookies");
Blazor Authentication
Blazor が使用する認証方式は ASP.NET Core と同じですが、Blazor WebAssembly と Blazor Server では異なります。前者の認証は、他のフロントエンドサイトと同様にバイパスされる可能性があります。クライアント側のプログラムはユーザーが改変できるため、データを送信する API 側でも認証が必要です。後者では、組み込みの AuthenticationStateProvider を使用して前述の HttpContext.User を取得できます。筆者は以前、この Service を継承してオーバーライドし、JWT 認証を実装しました。
AuthenticationStateProvider は、昨日説明した <AuthorizeView> や <CascadingAuthenticationState> が現在の認証状態を取得できる理由です。ただし、既定の認証機構をオーバーライドしないのであれば、Component 内で AuthenticationStateProvider を直接注入することは推奨しません。直接使用する場合の欠点は明らかで、現在の request の認証状態が変更されても、その Component の認証機構を変更したことになるため、その Component には変更が通知されません。
以下の図のように、ApiAuthenticationStateProvider は AuthenticationStateProvider を継承し、Task<AuthenticationState> をオーバーライドしています。このプロパティから現在の認証状態を取得できます。MarkUserAsAuthenticated() と MarkUserAsLoggedOut() は筆者が独自に作成したメソッドで、ユーザーが認証済みであることやログアウトをマークするためのものです。NotifyAuthenticationStateChanged() はその名の通り、各 Component に現在の認証状態を通知します。これが継承とオーバーライドの例です。

次の図は、Component 内で現在の request の HttpContext.User および Claims を取得する方法を示しています。ただし、ここではサービスを使用して User を取得し、その Claims を取得しているため、冗長な処理となっています。

単に HttpContext.User を取得するだけであれば、次の図のようにすれば十分です。Task<AuthenticationState> は [CascadingParameter] として階層的に渡されるからです。

参考文献:
注:本文のコードは .NET 6 + Visual Studio 2022 でリファクタリングしています。原文のリンクとリファクタリング後のコードを比較しながら学習していただければ幸いです。ご一読いただきありがとうございます。原著者をサポートします。