実務Webアプリ開発編です。今回は、Blazor(サーバー側)の認証の全体像を整理します。

前回でDBの初期データまで入れ、永続化のパートが終わりました。今回から認証のパートです。

以下のような方に役立つ内容となっています。

この記事が役に立つ方

  • Blazor(サーバー側)のCookie認証で、認証がどこで行われ、ログイン後の情報がどう保たれるのかを知りたい
  • ロールを変えたのに画面に反映されないなど、ログイン後の情報が古いまま残る理由を知りたい
  • 認証のコードをどの層に置くか、ログイン方式をあとから替えられるかで迷っている

以下のようなMentorAppを題材として進めます。

題材とするMentorApp

GitHubにドキュメント・コードの一式があります。

prota-p/MentorApp: Blazor Server × クリーンアーキテクチャ × DDD の学習用リファレンス実装(実務Webアプリ開発編の題材)

この回は全体像の回で、コードはほとんど読みません(見るのはインターフェースの宣言だけです)。認証がどこで起き、何を決めればよいかをつかみます。

プロ太

認証は方式も用語も多くて、迷いやすいところです。この記事では、決めることを3つの論点に整理します。まず仕組みの基本を押さえて、その3つを順に見ていきましょう。

Blazorの認証の基本

最初に、サーバー側で動くBlazorで、認証がどこで起きるかを押さえます。この基本が、あとの3つの論点すべての土台です。

ここでのサーバー側とは、ページの表示も操作もサーバーで処理する構成(静的SSRとInteractiveServer)です。ブラウザで動かすWebAssemblyは含みません。

認証はHTTPの要求で行い、回線はつないだときの結果を持ち続ける

先に、この記事で使う用語を4つ確認します。

用語意味
認証と認可認証は「誰か」を確かめること。認可は「何をしてよいか」を決めること。ロールは認可の材料
Cookieブラウザが保存し、同じサイトへの要求に自動で付ける小さなデータ。ログインすると、サーバーがログイン済みの印を入れる
クレームログイン中のユーザーについての情報の1項目(ID・名前・ロールなど)。その一式がClaimsPrincipal
回線(circuit)ブラウザとサーバーをつなぎっぱなしにする接続(SignalR)。ボタンなどの操作をサーバーへ送り、画面の差分を受け取る

認証は「誰か」を確かめること、認可は「何をしてよいか」を決めることです。この回の主役は認証で、ロールは認可の材料として出てきます。

Cookieは、ブラウザが保存して、同じサイトへの要求に自動で付ける小さなデータです。ログインすると、サーバーがここにログイン済みの印を入れます。

クレームは、ログイン中のユーザーについての情報の1項目(ID・名前・ロールなど)です。その一式がClaimsPrincipalで、Cookieにも暗号化して入ります。

回線(circuit)は、ブラウザとサーバーをつなぎっぱなしにする接続です。ボタンを押すと、操作がこの接続でサーバーへ届き、サーバーが処理して画面の差分を返します。

この記事は、図1の構成を前提にします。ページの枠は静的SSRで、その中のページの中身だけがInteractiveServerです。Blazor Web Appのテンプレートの既定の形です。

図1: この記事で前提とする構成
図1:この記事で前提とする構成

静的SSRは、サーバーがHTMLを作って返すだけの表示のしかたです。InteractiveServerは、回線でつながり、ボタンなどの操作にサーバーが応える表示のしかたです。

そのため、ページの枠は、開くときも移るときもHTTPの要求のたびに描かれ、ページの中身は、回線でつながって操作を受けます。

図2は、ログインからページの操作までの流れです。サーバー側は、HTTPの要求と回線の2つに分かれます。

図2: Blazor(サーバー側)の認証はどこで起きるか
図2:Blazor(サーバー側)の認証はどこで起きるか

ログイン(⓪)はHTTPの要求です。サーバーはここで本人を確かめ、クレームを入れたCookieを返します。

ページを開く(①)のもHTTPの要求です。ブラウザがCookieを付け、サーバーはCookieからクレームを取り出します。

そのクレームを見て、ページを開いてよいかを判断し、ロールに応じてメニューを出し分けるなど、そのユーザーに合わせたページを描きます。

続けて、ブラウザは回線をつなぎます(②)。これもHTTPの要求から始まり、サーバーはCookieを読んで、クレームを回線へ渡します。

回線は、このとき受け取ったクレームの写しを持ちます。

画面を操作する(③)と、操作は回線を通ってサーバーへ届きます。HTTPの要求ではないので、Cookieは読み直しません。回線は写しを使います。

リンクで別のページへ移る(④)と、①と同じHTTPの要求になり、サーバーはまたCookieを読みます。一方、回線はそのまま続きます。

このように、HTTPの要求は毎回Cookieからクレームを読み、回線はつないだときの写しを持ち続けます。

⓪では、パスワードなどで本人を確かめます。①②④では、サーバーが自分で発行したCookieを検証して、誰の要求かを判定します。

この、要求のたびにCookieで誰の要求かを確かめる仕組みを、Cookie認証と呼びます。「認証はHTTPの要求で行う」とは、この意味です。

プロ美

ログインしたあとも、ページを移るたびにCookieを読んでいるんだね。回線のほうは、ずっと最初の写しのまま?

プロ太

そうです。回線の写しは、既定では自動で更新されません。新しくなるのは、F5(ページの再読み込み)などで回線をつなぎ直したときです。

この仕組みから言えることが2つあります。1つは、ログインとログアウトはCookieを返す・消す処理なので、HTTPの要求で行うことです。

もう1つは、ロールや表示名のような変わりうる情報を、回線の写しのまま使ってよいか、判断が要ることです(MentorAppはDBから読みます。詳しくはこの先の回で)。

この上で決める3つの論点

ここまでが、Blazor(サーバー側)におけるCookie認証と回線の基本です。この基本の上で決めることを、次の表の3つの論点に整理し、1つずつ見ていきます。

論点主な選択肢
ログインの仕組みをどこで持つか自前で作る(ASP.NET Core Identity)/外部の認証サービス(IdP)に任せる(OIDC)
IdPのアカウントとアプリのユーザーをどう結ぶか外部IDで結ぶ。ロールはIdPで持つ/アプリのDBで持つ
認証をどの層に置くか画面やサービスが直接読む/実装はInfrastructureに置き、インターフェース越しに使う

ログインを自前でするか、外部に任せるか

1つ目は、ログインで本人を確かめる仕組みを、どこで持つかです。

論点主な選択肢
ログインの仕組みをどこで持つか自前で作る(ASP.NET Core Identity)/外部の認証サービス(IdP)に任せる(OIDC)

2つの選択肢

選択肢は、次の表の2つです。IdP(Identity Provider)は、本人を確かめる外部のサービスで、GoogleやMicrosoft Entra IDが代表です。

選択肢アプリが持つもの向いている場面
自前で作る(ASP.NET Core Identity)パスワード・多要素認証・パスワードのリセット・アカウントの停止を、自分のDBと画面で持つこのアプリだけでログインが完結する。利用者のデータを自分のサーバーに置きたい
外部のIdPに任せる(OIDC)持たない。IdPが本人を確かめ、アプリは本人のクレームだけを受け取る利用者がすでにアカウントを持っている。1つのアカウントで複数のアプリに入れるようにする

自前で作るなら、ASP.NET Core Identityが定番です。パスワードの保存や多要素認証、リセットの画面まで、アプリが自分で持ちます。

外部のIdPに任せるなら、OIDC(OpenID Connect)という標準の手順でIdPにログインしてもらい、アプリは本人のクレームだけを受け取ります。

Identityに外部ログインを足す、中間の形もあります。

公式は、そのアプリだけでログインが完結するならIdentityが最も簡単で、1つのアカウントで複数のアプリに入るなら外部が向く、としています。

それぞれの作り方は、入門編で扱っています。

自前で作る例です。ASP.NET Core Identityで、ユーザーの登録とログインを作ります。

【C#、Blazor】Webアプリ開発入門編(6)「Todoアプリ」へユーザ認証・管理機能をつける ~ASP.NET Core Identityの使い方~ Blazor編です。前回に引き続き、Todoリストアプリ開発を題材として、Blazorの基本機能を学びます。 初心者がBlazo...

外部のIdPに任せる例です。IdPの一つであるMicrosoft Entra IDで、ログインを付けます。

【C#、Blazor】Webアプリ開発入門編(Ex3)Microsoft Entra ID(旧AzureAD)~アプリへ簡単に認証機能を追加!~【ASP.NET Core】 Blazor編です。今回はC#BlazorアプリへMicrosoft EntraID(旧Azure Active Directory)...

MentorAppは本人を確かめる仕組みごと外部に任せる

MentorAppは、外部のIdPに任せます。パスワードも多要素認証もアカウントの停止もIdPの側で行い、アプリは本人を確かめる仕組みを持ちません。

選択肢アプリが持つもの向いている場面
外部のIdPに任せる(OIDC)持たない。IdPが本人を確かめ、アプリは本人のクレームだけを受け取る利用者がすでにアカウントを持っている。1つのアカウントで複数のアプリに入れるようにする

IdPに任せるのは、ログインのときに本人を確かめるところまでです。ログインしたあとは、アプリが自分のCookieでログイン状態を持ちます。

IdPのアカウントを、アプリのユーザーに結びつける

2つ目は、IdPのアカウントと、アプリのユーザーをどう結ぶかです。1つ目で、外部に任せるほうを選んだときに出てくる論点です。

論点主な選択肢
IdPのアカウントとアプリのユーザーをどう結ぶか外部IDで結ぶ。ロールはIdPで持つ/アプリのDBで持つ

外部IDとアプリのユーザー

IdPがアプリに渡すクレームは、アカウントのID(外部ID)と、名前・メールアドレスなどです。アプリで何者かは、IdPは知りません。

アプリのユーザー(DBの1行)とは、この外部IDで結びます。名前やメールは変わりうるので、結びつけには使いません。

知らない外部IDでログインされたときの扱いも決めます。初回ログインで自動で作るか、招待した人だけを通すかです。

ロールをどこで持つか

ロールの置き場は、次の表の2つです。

置き場特徴向いている場面
IdPで持つ(グループ・アプリロール)ログインのときにクレームとして届く。管理はIdPの管理画面で行う組織の部署や役職がそのまま権限になる。複数のアプリで同じ権限を使う
アプリのDBで持つアプリの画面で変えられる。IdPには手を入れないアプリ固有のロール。アプリの管理者が画面で変える

IdPで持つと、ログインのときにロールがクレームとして届きます。組織の部署や役職をそのまま権限にするアプリに向きます。

アプリのDBで持つと、アプリの画面で変えられます。アプリ固有のロールを、アプリの管理者が変えるなら、こちらです。

MentorAppの結びつけ方

MentorAppは、初回ログインでMenteeとしてユーザーを作り、ロールはDBで持ちます。管理者が画面でロールを変える仕様だからです。

置き場特徴向いている場面
アプリのDBで持つアプリの画面で変えられる。IdPには手を入れないアプリ固有のロール。アプリの管理者が画面で変える

最初の管理者だけは、ログインの前に設定の外部ID(ExternalId)から作ります。前回の初期管理者がそれです。

図3は、Googleでログインしたときの例です。Cookieには、IdPのクレーム(外部ID・名前・メール)がそのまま入ります。

図3: IdPのアカウントとアプリのユーザーの結びつき
図3:IdPのアカウントとアプリのユーザーの結びつき

外部IDは、Googleではsubです。表示名やロールといったアプリの情報は、Cookieに入れません。

アプリは、要求のたびにCookieの外部IDでDBを引き、表示名やロールをクレームに足します。DBの行には、アプリの主キーが別にあります。

IdPの名前が「Hanako Yamada」でも、表示名は「山田 花子」のままです。DBが正本なので、IdPの名前が変わっても追いません。

これがMentorAppの結びつけ方です。IdPとは外部IDだけで結び、アプリのユーザー(ID・表示名・ロール)はDBで持ちます。

プロ美

IdPは「誰か」を教えてくれるだけで、その人がアプリで何者かは、アプリのDBが決めるんだね。

プロ太

そのとおりです。だから、IdPを替えても、アプリのユーザーを持つ仕組みは変わりません。

認証をどの層に置くか

3つ目は、クリーンアーキテクチャで、認証をどの層に置くかです。

論点主な選択肢
認証をどの層に置くか画面やサービスが直接読む/実装はInfrastructureに置き、インターフェース越しに使う

認証は層を越えやすい

何も決めずに書くと、画面の部品がClaimsPrincipalからIdPのクレームを直接読む形になりがちです。

// 画面の部品やサービスが、IdPのクレーム名を直接読む
var user = (await AuthState).User;                      // 認証状態
var externalId = user.FindFirst("oid")?.Value;          // Entra ID の外部ID
var dbUser = await db.Users.SingleAsync(u => u.ExternalId == externalId);
if (dbUser.Role == Role.Admin) { /* 管理者だけの処理 */ }

この4行に、フレームワークの認証の型、IdPのクレーム名、DBへの問い合わせ、ロールの判断が混ざっています。

IdPをEntra IDからGoogleに替えるとクレーム名が変わり、読んでいる場所をすべて直すことになります。

認証は、ミドルウェア・フレームワークの型・DB・画面にまたがります。だから、どの層にも染み出しやすいのです。

MentorAppの分け方

MentorAppは、認証の部品を図4のように4層へ置きます。4層の役割は第15回で扱いました。

図4: 認証の部品を4層に置く
図4:認証の部品を4層に置く

認証の実装はInfrastructureに置きます。Cookie認証、DBからクレームを足す処理、ログイン方式の差し込み口です。

Applicationには、認証のインターフェースを2つ置きます。

Webがするのは2つだけです。起動時に設定の値を渡すことと、画面の部品がICurrentUserService越しに操作している人の情報を読むことです。

ICurrentUserServiceは、Infrastructureが実装します。Applicationが口を決め、Infrastructureが中身を書く、第22回の依存性逆転の形です。

IUserAuthenticationServiceは逆に、Applicationが実装します。中身が「初回ログインならMenteeとして作る」という業務のルールだからです。

Infrastructureは、クレームを足すときにこれを使う側です。Webが画面からApplicationを使うのと同じで、依存はどちらもApplicationに向きます。

Domainのユーザーとロールは、認証を知りません。

認証の実装はInfrastructureに置き、ほかの層はインターフェース越しに使う。これが図4の分け方です。

ICurrentUserServiceの宣言です。メソッドは1つだけです。

using MentorApp.Domain.Models.Users;

namespace MentorApp.Application.Contracts.Authentication;

public record CurrentUser(
    Guid UserId,
    string ExternalId,
    string DisplayName,
    Role Role);

// …(コメント・10 行省略)
public interface ICurrentUserService
{
    public Task<CurrentUser?> GetCurrentUserAsync();
}

返すのは、操作している人の情報(ID・外部ID・表示名・ロール)です。コードではCurrentUserという型です。

クレームをそのまま渡さず、アプリが使う4項目にまとめた形です。認証の型は宣言にも返り値にも出ず、Webはクレームの名前を知らずに済みます。

ログイン方式を取り替えられるようにする

図5は、Infrastructureの中を開いた図です。Cookie認証やDBからクレームを足す処理は、ログイン方式によらない共通の部品です。

図5: ログイン方式の差し込み口と、方式ごとの部品
図5:ログイン方式の差し込み口と、方式ごとの部品

方式ごとに違うのは、ログインで本人を確かめる部分と、IdPごとのクレーム名の対応です。Mock・Google・Entra IDの3つがあり、どれもIProviderSetupの実装です。

設定のProviderで1つを選び、差し込み口に挿します。新しいIdPを足すときも、方式ごとの部品を1つ書き、選べる一覧に登録して、設定で選びます。

{
    …(DBの設定・4 行省略)
    "Authentication": {
        "Provider": "Mock",
        …(初期管理者・5 行省略)
        "Providers": {
            "EntraId": {
                "TenantId": "YOUR_TENANT_ID",
                "ClientId": "YOUR_CLIENT_ID",
                "ClientSecret": "YOUR_CLIENT_SECRET"
            },
            "Google": {
                "ClientId": "YOUR_GOOGLE_CLIENT_ID",
                "ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET"
            }
        }
    },
    …(ログの設定・8 行省略)
}

ログイン方式を変えても、取り替えるのは差し込み口に挿す1つだけです。共通の部品は、そのまま使います。

取り替えられるから、開発用のMockを置けます。開発環境のログイン画面には、IdPを通さずに入るボタンが3つあります。

開発環境のログイン画面(Mock)

IdPの設定なしで開発や画面を通したテスト(E2E)ができ、ボタン1つで管理者・メンター・メンティーを切り替えて試せます。

ただし、誰にでもなりすませる方式です。本番の環境でMockが選ばれていたら、MentorAppは起動せずに止まります。

プロ美

本物のIdPを用意しなくても、認証の仕組みは全部動かせるってこと?

プロ太

ログイン以外は全部です。だからこの先の回も、Mockのまま読み進められます。

補足:認証の設計に決まった答えがない理由

層に分ける設計では、認証は層の交差点になります。ミドルウェア、クレーム、DB、誰の操作かを見て進める処理、ロールが、層をまたいで関わるからです。

そのため、どの層に何を置くかは、設計の流儀によって判断が分かれます。この記事の分け方は、設計の一例です。

もう1つ、ロールの変更をいつ反映するかも、Web開発一般のトレードオフです。Cookieの期限まで待つ、要求ごとにDBを見る、などの選択があります。

Blazor(サーバー側)では、ここに回線が加わります。要求の時点で作ったクレームが、操作の時点では古くなりうるからです。

まとめ

今回は、Blazor(サーバー側)のCookie認証の基本と、その上で決める3つの論点を整理しました。重要なポイントは以下です。

今回のポイント

  • 基本:HTTPの要求のたびにCookieからクレームを読む(Cookie認証)。回線は、つないだときの写しを持ち続ける(自動では新しくならない)
  • 論点1:MentorAppは、ログインで本人を確かめる仕組みを、外部のIdPに任せる
  • 論点2:IdPのアカウントとは外部IDだけで結び、アプリのユーザーとロールはDBで持つ
  • 論点3:認証の実装はInfrastructureに置き、ほかの層はインターフェース越しに使う。ログイン方式は差し込み口で取り替える

次回からは、今回の仕組みを1つずつ、コードを見ながら詳しく見ていきます。まずMockのままで進め、最後にログインだけを外部のIdPに取り替えます。

プロ太

認証はHTTPの要求のたびに行い、回線はつないだときの結果を持ち続ける。この基本を押さえておけば、この先の回で迷いません。

引き続き、Webアプリ・Blazorの認証の仕組みについて一緒に学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。