次の方法で共有


Microsoft Entra ID を使用して ASP.NET Core Blazor Web App をセキュリティで保護する

この記事では、サンプル アプリを使用して、Microsoft identity プラットフォーム/Microsoft Identity Web パッケージ Microsoft Entra ID を使用してBlazor Web Appをセキュリティで保護する方法について説明します。

以下の仕様をカバーします。

  • Blazor Web Appでは、グローバル対話機能 (InteractiveAuto)を持つ Auto レンダリング モードが使用されます。
  • サーバー プロジェクトは AddAuthenticationStateSerialization を呼び出して、 PersistentComponentState を使用して認証状態をクライアントにフローするサーバー側認証状態プロバイダーを追加します。 クライアントは AddAuthenticationStateDeserialization を呼び出して、サーバーによって渡された認証状態を逆シリアル化して使用します。 認証状態は、WebAssembly アプリケーションの有効期間中は変わりません。
  • アプリでは、microsoft Identity Web パッケージに基づいて、Microsoft Entra ID を使用します。
  • 非対話型トークンの自動更新は、フレームワークによって管理されます。
  • このアプリでは、サーバー側とクライアント側のサービスの抽象化を使用して、生成された気象データを表示します。
    • Weather コンポーネントをサーバーにレンダリングして気象データを表示する場合、コンポーネントはサーバー上のServerWeatherForecasterを使用して(Web API 呼び出しを介してではなく) 気象データを直接取得します。
    • Weather コンポーネントがクライアントにレンダリングされると、コンポーネントは ClientWeatherForecaster サービス実装を使用します。この実装では、事前構成済みのHttpClient (クライアント プロジェクトのProgram ファイル内) を使用して、気象データ用にサーバー プロジェクトの Minimal API (/weather-forecast) への Web API 呼び出しが行われます。 Minimal API エンドポイントは、 ServerWeatherForecaster クラスから気象データを取得し、コンポーネントによってレンダリングするためにクライアントに返します。

サンプル アプリ

サンプル アプリは、以下の 2 つのプロジェクトで構成されます。

  • BlazorWebAppEntra: 気象データ用の Minimal API エンドポイントの例を含む、Blazor Web App のサーバー側プロジェクト。
  • BlazorWebAppEntra.Client: Blazor Web App のクライアント側プロジェクト。

次のリンクを使用して、リポジトリのルートから最新バージョンのフォルダーからサンプル アプリにアクセスします。 プロジェクトは、.NET 9 以降の BlazorWebAppEntra フォルダーにあります。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

サーバー側の Blazor Web App プロジェクト (BlazorWebAppEntra)

BlazorWebAppEntra プロジェクトは、Blazor Web App のサーバー側プロジェクトです。

BlazorWebAppEntra.http ファイルは、気象データの要求のテストに使用できます。 エンドポイントをテストするには BlazorWebAppEntra プロジェクトが実行されている必要があり、エンドポイントはファイルにハードコーディングされていることに注意してください。 詳細については、「Visual Studio 2022 で .http ファイルを使う」を参照してください。

クライアント側 Blazor Web App プロジェクト (BlazorWebAppEntra.Client)

BlazorWebAppEntra.Client プロジェクトは、Blazor Web App のクライアント側プロジェクトです。

ユーザーがクライアント側のレンダリング中にログインまたはログアウトする必要がある場合は、ページ全体の再読み込みが開始されます。

構成

このセクションでは、サンプル アプリを構成する方法について説明します。

AddMicrosoftIdentityWebAppfrom Microsoft Identity Web (Microsoft.Identity.Web NuGet パッケージAPI ドキュメント) は、サーバー プロジェクトのappsettings.json ファイルの AzureAd セクションによって構成されます。

Entra または Azure portal でのアプリの登録で、https://localhost/signin-oidcRedirect URI を使用して Web プラットフォーム構成を使用します (ポートは必要ありません)。 ID トークンアクセス トークンImplicit 許可フローとハイブリッド フローが選択されていないこと確認します。 OpenID Connect ハンドラーは、承認エンドポイントから返されたコードを使用して、適切なトークンを自動的に要求します。

Configure the app

サーバー プロジェクトのアプリ設定ファイル (appsettings.json) で、アプリの AzureAd セクションの構成を指定します。 Entra または Azure portal でアプリの登録から、アプリケーション (クライアント) ID、テナント (発行元) ドメイン、およびディレクトリ (テナント) ID を取得します。

"AzureAd": {
  "CallbackPath": "/signin-oidc",
  "ClientId": "{CLIENT ID}",
  "Domain": "{DOMAIN}",
  "Instance": "https://login.microsoftonline.com/",
  "ResponseType": "code",
  "TenantId": "{TENANT ID}"
},

前の例のプレースホルダー:

  • {CLIENT ID}: アプリケーション (クライアント) ID。
  • {DOMAIN}: テナント (パブリッシャー) ドメイン。
  • {TENANT ID}: ディレクトリ (テナント) ID。

例:

"AzureAd": {
  "CallbackPath": "/signin-oidc",
  "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "Domain": "contoso.onmicrosoft.com",
  "Instance": "https://login.microsoftonline.com/",
  "ResponseType": "code",
  "TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
},

コールバック パス (CallbackPath) は、Entra または Azure portal でアプリケーションを登録するときに構成されたリダイレクト URI (ログイン コールバック パス) と一致する必要があります。 パスは、アプリの登録の Authentication ブレードで構成されます。 CallbackPathの既定値は、https://localhost/signin-oidcの登録済みリダイレクト URI に対して/signin-oidcされます (ポートは必要ありません)。

警告

アプリ シークレット、接続文字列、資格情報、パスワード、個人識別番号 (PIN)、プライベート C#/.NET コード、秘密キー/トークンをクライアント側コードに格納しないでください。これは安全ではありません。 テスト/ステージング環境と運用環境では、サーバー側の Blazor コードと Web API は、プロジェクト コードまたは構成ファイル内で資格情報を維持しないように、セキュリティで保護された認証フローを使用する必要があります。 ローカル開発テスト以外では、環境変数が最も安全なアプローチではないため、環境変数を使用して機密データを格納しないようにすることをお勧めします。 ローカル開発テストでは、機密データをセキュリティで保護するために、 Secret Manager ツール をお勧めします。 詳細については、「 機密データと資格情報を安全に管理するを参照してください。

クライアント シークレットを確立する

Entra または Azure portal のアプリの Entra ID 登録でクライアント シークレットを作成します (Manage>Certificates & secrets>New クライアント シークレット)。 次のガイダンスでは、新しいシークレットの Value を使用します。

次のいずれかの方法または両方の方法を使用して、クライアント シークレットをアプリに提供します。

  • シークレット マネージャー ツール: シークレット マネージャー ツールは、ローカル コンピューターにプライベート データを格納し、ローカル開発時にのみ使用されます。
  • Azure Key Vault: ローカルで作業する場合の開発環境を含め、任意の環境で使用するために、クライアント シークレットをキー コンテナーに格納できます。 一部の開発者は、ステージングおよび運用環境のデプロイにキー コンテナーを使用し、ローカル開発に Secret Manager ツール を使用することを好みます。

クライアント シークレットをプロジェクト コードまたは構成ファイルに格納しないことを強くお勧めします。 このセクションの方法のいずれかまたは両方など、セキュリティで保護された認証フローを使用します。

シークレット マネージャー ツール

Secret Manager ツールは、サーバー アプリのクライアント シークレットを構成キー AzureAd:ClientSecretの下に格納できます。

サンプル アプリはシークレット マネージャー ツール用に初期化されていません。 Visual Studio の Developer PowerShell コマンド シェルなどのコマンド シェルを使用して、次のコマンドを実行します。 コマンドを実行する前に、 cd コマンドを使用してサーバー プロジェクトのディレクトリにディレクトリを変更します。 このコマンドは、サーバー アプリのプロジェクト ファイルにユーザー シークレット識別子 (<UserSecretsId>) を確立します。これは、アプリのシークレットを追跡するためにツールによって内部的に使用されます。

dotnet user-secrets init

次のコマンドを実行して、クライアント シークレットを設定します。 {SECRET} プレースホルダーは、アプリの Entra 登録から取得したクライアント シークレットです。

dotnet user-secrets set "AzureAd:ClientSecret" "{SECRET}"

Visual Studio を使用している場合は、ソリューション エクスプローラーでサーバー プロジェクトを右クリックし、[ユーザー シークレットの管理] を選択することで、シークレットが設定されていることを確認できます

Azure Key Vault

Azure Key Vault は、アプリのクライアント シークレットをアプリに提供するための安全なアプローチを提供します。

キー コンテナーを作成してクライアント シークレットを設定するには、「 Azure Key Vault シークレットについて (Azure ドキュメント)」を参照してください。Azure Key Vault の使用を開始するためにリソースをクロスリンクします。 このセクションのコードを実装するには、キー コンテナーとシークレットを作成するときに、Azure のキー コンテナー URI とシークレット名を記録します。 Access ポリシー パネルでシークレットのアクセス ポリシーを設定する場合:

  • Get シークレットのアクセス許可のみが必要です。
  • シークレットの Principal としてアプリケーションを選択します。

重要

キー コンテナー シークレットは、有効期限が設定された状態で作成されます。 キー コンテナー シークレットの有効期限が切れるタイミングを追跡し、その日付が経過する前にアプリの新しいシークレットを作成してください。

次の GetKeyVaultSecret メソッドは、キー コンテナーからシークレットを取得します。 このメソッドをサーバー プロジェクトに追加します。 プロジェクトの名前空間スキームに合わせて名前空間 (BlazorSample.Helpers) を調整します。

Helpers/AzureHelper.cs:

using Azure;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string tenantId, string vaultUri, string secretName)
    {
        DefaultAzureCredentialOptions options = new()
        {
            // Specify the tenant ID to use the dev credentials when running the app locally
            // in Visual Studio.
            VisualStudioTenantId = tenantId,
            SharedTokenCacheTenantId = tenantId
        };

        var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential(options));
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}

サービスがサーバー プロジェクトの Program ファイルに登録されている場合は、次のコードを使用してクライアント シークレットを取得して適用します。

var tenantId = builder.Configuration.GetValue<string>("AzureAd:TenantId")!;
var vaultUri = builder.Configuration.GetValue<string>("AzureAd:VaultUri")!;
var secretName = builder.Configuration.GetValue<string>("AzureAd:SecretName")!;

builder.Services.Configure<MicrosoftIdentityOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
    {
        options.ClientSecret = 
            AzureHelper.GetKeyVaultSecret(tenantId, vaultUri, secretName);
    });

上記のコードが動作する環境を制御する場合 (たとえば、 Secret Manager ツールを使用してローカル開発を選択したためにコードをローカルで実行しないようにする場合 、環境をチェックする条件付きステートメントで上記のコードをラップできます。

if (!context.HostingEnvironment.IsDevelopment())
{
    ...
}

appsettings.jsonAzureAd セクションで、次のVaultUriSecretName構成キーと値を追加します。

"VaultUri": "{VAULT URI}",
"SecretName": "{SECRET NAME}"

前の例の場合:

  • {VAULT URI} プレースホルダーは、キー コンテナーの URI です。 URI に末尾のスラッシュを含めます。
  • {SECRET NAME} プレースホルダーはシークレット名です。

例:

"VaultUri": "https://contoso.vault.azure.net/",
"SecretName": "BlazorWebAppEntra"

構成は、アプリの環境構成ファイルに基づいて専用のキー コンテナーとシークレット名を簡単に指定するために使用されます。 たとえば、開発中の appsettings.Development.json 、ステージング時の appsettings.Staging.json 、運用デプロイの appsettings.Production.json に対して、さまざまな構成値を指定できます。 詳しくは、「ASP.NET Core Blazor の構成」をご覧ください。

サインアウト時に home ページにリダイレクトする

ユーザーがアプリ内を移動すると、LogInOrOut コンポーネント (Layout/LogInOrOut.razor) によって、戻り URL (ReturnUrl) の非表示フィールドが現在の URL (currentURL) の値に設定されます。 ユーザーがアプリからサインアウトすると、identity プロバイダーはサインアウト元のページにユーザーを返します。

ユーザーがセキュリティで保護されたページからサインアウトする場合、サインアウト後に同じセキュリティで保護されたページに戻され、認証プロセスを再び受けることになります。 この動作は、ユーザーが頻繁にアカウントを切り替える必要がある場合には問題ありません。 ただし、別のアプリ仕様では、ユーザーがサインアウト後にアプリの home ページまたはその他のページに戻る必要がある場合があります。次の例は、アプリの home ページをサインアウト操作の戻り URL として設定する方法を示しています。

次の例では、LogInOrOut コンポーネントに対する重要な変更が示されています。 既定のパスであるため、/のhome ページに設定されているReturnUrlの非表示フィールドを指定する必要はありません。 IDisposable は実装されなくなりました。 NavigationManager は挿入されなくなりました。 @code ブロック全体が削除されます。

Layout/LogInOrOut.razor:

@using Microsoft.AspNetCore.Authorization

<div class="nav-item px-3">
    <AuthorizeView>
        <Authorized>
            <form action="authentication/logout" method="post">
                <AntiforgeryToken />
                <button type="submit" class="nav-link">
                    <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true">
                    </span> Logout @context.User.Identity?.Name
                </button>
            </form>
        </Authorized>
        <NotAuthorized>
            <a class="nav-link" href="authentication/login">
                <span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> 
                Login
            </a>
        </NotAuthorized>
    </AuthorizeView>
</div>

トラブルシューティング

ログ機能

サーバー アプリは標準の ASP.NET Core アプリです。 サーバー アプリでより下位のログ レベルを有効にするには、ASP.NET Core ログのガイダンスを参照してください。

Blazor WebAssembly 認証のデバッグまたはトレース ログを有効にするには、記事バージョン セレクターを ASP.NET Core 7.0 以降に設定して、ASP.NET Core Blazor ログの "クライアント側認証ログ" セクションを参照してください。

一般的なエラー

  • アプリまたは Identity プロバイダー (IP) の構成の誤り

    最も一般的なエラーの原因は、構成の誤りです。 以下に例を示します。

    • シナリオの要件によっては、権限、インスタンス、テナント ID、テナント ドメイン、クライアント ID、またはリダイレクト URI の欠落または誤りによって、アプリによるクライアントの認証ができなくなります。
    • 要求スコープが正しくないと、クライアントはサーバー Web API エンドポイントにアクセスできません。
    • サーバー API のアクセス許可が正しくないか、存在しないと、クライアントがサーバー Web API エンドポイントにアクセスできなくなります。
    • IP のアプリ登録のリダイレクト URI で構成されているものとは異なるポートでアプリが実行されています。 Microsoft Entra ID と、localhost 開発テスト アドレスで実行されるアプリにポートは必要ありませんが、アプリのポート構成とアプリが実行されているポートは、localhost 以外のアドレスと一致する必要があることに注意してください。

    この記事の構成範囲では、正しい構成の例を示しています。 アプリと IP の構成に誤りがないか、構成を慎重に確認してください。

    構成が正しい場合:

    • アプリケーション ログを分析します。

    • ブラウザーの開発者ツールを使用して、クライアント アプリと IP またはサーバー アプリの間のネットワーク トラフィックを確認します。 多くの場合、要求を行った後、IP またはサーバー アプリによって、問題の原因を特定する手掛かりを含む正確なエラー メッセージまたはメッセージがクライアントに返されます。 開発者ツールのガイダンスは、次の記事にあります。

    ドキュメント チームは、ドキュメントのフィードバックと記事のバグについては対応します (こちらのページのフィードバック セクションからイシューを作成してください) が、製品サポートを提供することはできません。 アプリのトラブルシューティングに役立つ、いくつかのパブリック サポート フォーラムが用意されています。 次をお勧めします。

    上記のフォーラムは、Microsoft が所有または管理するものではありません。

    セキュリティで保護されておらず、機密でも社外秘でもない再現可能なフレームワークのバグ レポートについては、ASP.NET Core 製品単位でイシューを作成してください。 問題の原因を徹底的に調査し、パブリック サポート フォーラムのコミュニティの助けを借りてもお客様自身で解決できない場合にのみ、製品単位でイシューを作成してください。 単純な構成の誤りやサードパーティのサービスに関連するユース ケースによって破損した個々のアプリのトラブルシューティングは、製品単位で行うことはできません。 レポートが機密性の高い性質のものである場合や、攻撃者が悪用するおそれのある製品の潜在的なセキュリティ上の欠陥が記述されている場合は、「セキュリティの問題とバグの報告」 (dotnet/aspnetcore GitHub リポジトリ) をご覧ください。

  • ME-ID で承認されないクライアント

    情報:Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 承認に失敗しました。 次の要件が満たされていません。DenyAnonymousAuthorizationRequirement:認証済みユーザーが必要です。

    ME-ID からのログイン コールバック エラー:

    • エラー: unauthorized_client
    • 説明: AADB2C90058: The provided application is not configured to allow public clients.

    このエラーを解決するには:

    1. Azure portal で、アプリのマニフェストにアクセスします。
    2. allowPublicClient 属性null または true に設定します。

Cookie とサイト データ

Cookie とサイト データは、アプリが更新されても保持され、テストやトラブルシューティングに影響する可能性があります。 アプリ コードの変更、プロバイダーによるユーザー アカウントの変更、プロバイダー アプリの構成変更を行うときは、次のものをクリアしてください。

  • ユーザーのサインインの Cookie
  • アプリの Cookie
  • キャッシュおよび保存されたサイト データ

残った Cookie とサイト データがテストとトラブルシューティングに影響しないようにする方法を、次に示します。

  • ブラウザーを構成する
    • ブラウザーが閉じるたびに cookie とサイト データをすべて削除するように構成できることをテストするために、ブラウザーを使用します。
    • アプリ、テスト ユーザー、プロバイダー構成が変更されるたびにブラウザーが手動で、または IDE によって閉じられていることを確認します。
  • カスタム コマンドを使用して、Visual Studio でブラウザーを InPrivate または Incognito モードで開きます:
    • Visual Studio の [実行] ボタンをクリックして [ブラウザーの選択] ダイアログボックスを開きます。
    • [追加] ボタンを選びます。
    • [プログラム] フィールドでブラウザーのパスを指定します。 次の実行可能パスが、Windows 10 の一般的なインストール場所です。 ブラウザーが別の場所にインストールされている場合、または Windows 10 を使用していない場合は、ブラウザーの実行可能ファイルのパスを指定してください。
      • Microsoft Edge: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
      • Google Chrome: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
      • Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
    • [引数] フィールドに、ブラウザーを InPrivate または Incognito モードで開くために使用するコマンドライン オプションを指定します。 ブラウザーによっては、アプリの URL が必要になる場合があります。
      • Microsoft Edge:-inprivate を使用してください。
      • Google Chrome: --incognito --new-window {URL} を使用します。プレースホルダー {URL} は開く URL (たとえば、https://localhost:5001 など) です。
      • Mozilla Firefox: -private -url {URL} を使用します。プレースホルダー {URL} は開く URL (たとえば、https://localhost:5001 など) です。
    • [フレンドリ名] フィールドに名前を指定します。 たとえば、Firefox Auth Testing のようにします。
    • [OK] ボタンを選択します。
    • アプリでテストを繰り返すたびにブラウザー プロファイルを選択する必要がないようにするには、 [既定値として設定] ボタンでプロファイルを既定値として設定します。
    • アプリ、テスト ユーザー、またはプロバイダー構成が変更されるたびに、ブラウザーが IDE によって閉じられていることを確認します。

アプリのアップグレード

開発マシンで .NET Core SDK をアップグレードしたり、アプリ内のパッケージ バージョンを変更したりした直後に、機能しているアプリが失敗することがあります。 場合によっては、パッケージに統一性がないと、メジャー アップグレード実行時にアプリが破壊されることがあります。 これらの問題のほとんどは、次の手順で解決できます。

  1. コマンド シェルから dotnet nuget locals all --clear を実行して、ローカル システムの NuGet パッケージ キャッシュをクリアします。
  2. プロジェクトのフォルダー binobj を削除します。
  3. プロジェクトを復元してリビルドします。
  4. アプリを再展開する前に、サーバー上の展開フォルダー内のすべてのファイルを削除します。

Note

アプリのターゲット フレームワークと互換性のないパッケージ バージョンの使用はサポートされていません。 パッケージの詳細については、NuGet ギャラリーまたは FuGet パッケージ エクスプローラーを使用してください。

サーバー アプリを実行する

Blazor Web App のテストとトラブルシューティングを行うときは、サーバー プロジェクトからアプリを実行していることを確認してください。

ユーザーを検査する

次の UserClaims コンポーネントは、アプリ内で直接使うことも、さらにカスタマイズするための基礎として使うこともできます。

UserClaims.razor:

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li><b>@claim.Type:</b> @claim.Value</li>
        }
    </ul>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthState == null)
        {
            return;
        }

        var authState = await AuthState;
        claims = authState.User.Claims;
    }
}

その他のリソース