OpenID Connect (OIDC) を使用して ASP.NET Core Blazor Web アプリをセキュリティで保護する

この記事では、dotnet/blazor-samples GitHub リポジトリ (.NET 8 以降) のサンプル アプリを使用して、OpenID Connect (OIDC) で Blazor Web アプリをセキュリティ保護する方法について説明します (サンプル アプリのダウンロード方法はこちらをご覧ください)。

このバージョンの記事では、Backend for Frontend (BFF) パターンを採用せずに OIDC を実装する方法について説明します。 BFF パターンは、外部サービスに対して認証された要求を行う場合に役立ちます。 アプリの仕様上 BFF パターンの採用が必要な場合は、記事のバージョン セレクターを OIDC と BFF パターンに切り替えてください。

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

  • Blazor Web アプリは、グローバル インタラクティビティを持つ自動レンダリング モードを使用します。
  • ユーザーの認証状態をキャプチャし、それをサーバーとクライアントの間でフローするために、サーバーおよびクライアント アプリによってカスタム認証状態プロバイダー サービスが使用されます。
  • このアプリは、すべての OIDC 認証フローの開始点です。 OIDC は、アプリ内で手動により構成され、Microsoft Entra IDMicrosoft Identity Web パッケージに依存せず、サンプル アプリも Microsoft Azure ホスティングを必要としません。 ただし、サンプル アプリは Entra や Microsoft Identity Web で使用でき、Azure でホストできます。
  • 非対話形式でのトークンの自動更新。
  • データのサーバー プロジェクトで (Web) API を安全に呼び出します。

サンプル アプリ

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

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

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

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

サーバー側 Blazor Web アプリ プロジェクト (BlazorWebAppOidc)

BlazorWebAppOidc プロジェクトは、Blazor Web アプリのサーバー側プロジェクトです。

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

Note

サーバー プロジェクトは IHttpContextAccessor/HttpContext を使いますが、対話形式でレンダリングされるコンポーネントには使いません。 詳細については、対話型サーバー側の ASP.NET Core Blazor レンダリングの脅威軽減策に関するガイダンスに関する記事を参照してください。

構成

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

Note

Microsoft Entra ID と Azure AD B2C の場合は、Microsoft Identity Web (Microsoft.Identity.Web NuGet パッケージAPI ドキュメント) の AddMicrosoftIdentityWebApp を使用できます。これにより、適切な既定値で OIDC ハンドラーと Cookie 認証ハンドラーの両方が追加されます。 このセクションのサンプル アプリとガイダンスでは、Microsoft Identity Web は使用しません。 このガイダンスでは、任意の OIDC プロバイダー用の OIDC ハンドラーを "手動で" 構成する方法を示します。 Microsoft Identity Web の実装の詳細については、リンクされているリソースを参照してください。

次の OpenIdConnectOptions の構成は、プロジェクトの Program ファイルに含まれる AddOpenIdConnect の呼び出しにあります。

  • SignInScheme: 認証の成功後に、ユーザーの ID の保持を担当するミドルウェアに対応する認証スキームを設定します。 OIDC ハンドラーでは、複数の要求にわたってユーザー資格情報を保持できるサインイン スキームを使用する必要があります。 次の行は、デモンストレーションの目的のためだけに存在します。 省略すると、DefaultSignInScheme がフォールバック値として使用されます。

    oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
  • openidprofile のスコープ (Scope) (省略可能): openidprofile スコープも、OIDC ハンドラーが機能するために必要なので既定で構成されますが、スコープが Authentication:Schemes:MicrosoftOidc:Scope の構成に含まれている場合は、これらを追加し直すことが必要になる場合があります。 一般的な構成のガイダンスについては、「ASP.NET Core の構成」と「ASP.NET Core Blazor の構成」を参照してください。

    oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
    
  • SaveTokens: 認可が成功した後で、アクセス トークンと更新トークンを AuthenticationProperties 内に保存する必要があるかどうかを定義します。 このプロパティは、最終的な認証 cookie のサイズを小さくするために、既定で false に設定されます。

    oidcOptions.SaveTokens = false;
    
  • オフライン アクセスのスコープ (Scope): offline_access スコープが更新トークンのために必要です。

    oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
    
  • AuthorityClientId: OIDC 呼び出しのためのオーソリティとクライアント ID を設定します。

    oidcOptions.Authority = "{AUTHORITY}";
    oidcOptions.ClientId = "{CLIENT ID}";
    

    例:

    • オーソリティ ({AUTHORITY}): https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/ (テナント ID a3942615-d115-4eb7-bc84-9974abcf5064 を使用)
    • クライアント ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    oidcOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    oidcOptions.ClientId = "4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    Microsoft Azure の "common" オーソリティの例:

    "common" オーソリティは、マルチテナント アプリに対して使用する必要があります。 "common" オーソリティをシングルテナント アプリに対して使用することもできますが、このセクションで後ほど示すように、カスタムの IssuerValidator が必要になります。

    oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
    
  • ClientSecret: OIDC クライアント シークレット。

    次の例は、テストとデモンストレーションの目的のためだけのものです。 クライアント シークレットをアプリのアセンブリ内に保存したり、シークレットをソース管理にチェックインしたりしないでください。 クライアント シークレットは、ユーザー シークレットAzure Key Vault、または環境変数の中に保存します。

    認証スキームの構成は、builder.Configuration["Authentication:Schemes:{SCHEME NAME}:{PropertyName}"] から自動的に読み取られます。ここでプレースホルダーの {SCHEME NAME} はスキームであり、既定では MicrosoftOidc です。 構成は事前に構成されているため、クライアント シークレットは、Authentication:Schemes:MicrosoftOidc:ClientSecret 構成キーを介して自動的に読み取ることができます。 環境変数を使用するサーバーでは、以下のように環境変数に Authentication__Schemes__MicrosoftOidc__ClientSecret という名前を付けます。

    set Authentication__Schemes__MicrosoftOidc__ClientSecret={CLIENT SECRET}
    

    デモンストレーションとテストに限りClientSecret を直接設定することができます。 デプロイされた運用アプリでは、値を直接設定しないでください。 若干のセキュリティの向上のためには、以下のように DEBUG シンボルを使用してこの行を条件付きでコンパイルします

    #if DEBUG
    oidcOptions.ClientSecret = "{CLIENT SECRET}";
    #endif
    

    例:

    クライアント シークレット ({CLIENT SECRET}): 463471c8c4...f90d674bc9 (表示用に短縮されています)

    #if DEBUG
    oidcOptions.ClientSecret = "463471c8c4...137f90d674bc9";
    #endif
    
  • ResponseType: 認可コード フローのみを実行するように、OIDC ハンドラーを構成します。 このモードでは、暗黙的な許可とハイブリッド フローは必要ありません。

    Entra または Azure portal の暗黙的な許可とハイブリッド フローのアプリ登録構成では、アクセス トークンまたは ID トークンを返すための承認エンドポイントのチェックボックスはどちらも選択*しないでください。 OIDC ハンドラーは、承認エンドポイントから返されたコードを使用して適切なトークンを自動的に要求します。

    oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
    
  • MapInboundClaims および NameClaimTypeRoleClaimType の構成: 多くの OIDC サーバーは、ClaimTypes の既定値 SOAP/WS-Fed ではなく、"name" と "role" を使用します。 MapInboundClaimsfalse に設定されていると、ハンドラーは要求のマッピングを実行せず、JWT からの要求名がアプリによって直接使用されます。 次の例では、名前とロールの要求を手動でマップします。

Note

ほとんどの OIDC プロバイダーでは、MapInboundClaimsfalse に設定する必要があります。こうすることで、要求の名前は変更されなくなります。

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
oidcOptions.TokenValidationParameters.RoleClaimType = "role";
  • パスの構成: パスは、OIDC プロバイダーにアプリケーションを登録するときに構成された、リダイレクト URI (ログイン コールバック パス) およびログアウト後リダイレクト (サインアウト コールバック パス) パスと、一致している必要があります。 Azure portal では、パスはアプリの登録の [認証] ブレードで構成されます。 サインインとサインアウト両方のパスを、リダイレクト URI として登録する必要があります。 既定値は、/signin-oidc/signout-callback-oidc です。

    • CallbackPath: ユーザーエージェントが返される、アプリのベース パス内の要求パス。

      Entra または Azure portal で、以下のように Web プラットフォーム構成のリダイレクト URI 内のパスを設定します。

      https://localhost/signin-oidc

      Note

      Microsoft Entra ID を使う場合、localhost アドレスにポートは必要ありません。 他のほとんどの OIDC プロバイダーでは、正しいポートが必要です。

    • SignedOutCallbackPath: ID プロバイダーからサインアウトした後でユーザー エージェントが返される、アプリのベース パス内の要求パス。

      Entra または Azure portal で、以下のように Web プラットフォーム構成のリダイレクト URI 内のパスを設定します。

      https://localhost/signout-callback-oidc

      Note

      Microsoft Entra ID を使う場合、localhost アドレスにポートは必要ありません。 他のほとんどの OIDC プロバイダーでは、正しいポートが必要です。

      Note

      Microsoft Identity Web を使用していて、microsoftonline.com オーソリティ (https://login.microsoftonline.com/{TENANT ID}/v2.0/) が使用されている場合、プロバイダーは現在、SignedOutCallbackPath にのみリダイレクトを行います。 Microsoft Identity Web で "common" オーソリティを使用できる場合は、この制限は存在しません。 詳細については、「オーソリティ URL にテナント ID が含まれていると postLogoutRedirectUri が機能しない (AzureAD/microsoft-authentication-library-for-js #5783)」を参照してください。

    • RemoteSignOutPath: このパスで要求を受け取ると、ハンドラーはサインアウト スキームを使用してサインアウトを呼び出します。

      Entra または Azure portal で、以下のようにフロントチャネル ログアウト URL を設定します。

      https://localhost/signout-oidc

      Note

      Microsoft Entra ID を使う場合、localhost アドレスにポートは必要ありません。 他のほとんどの OIDC プロバイダーでは、正しいポートが必要です。

    oidcOptions.CallbackPath = new PathString("{PATH}");
    oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
    oidcOptions.RemoteSignOutPath = new PathString("{PATH}");
    

    例 (既定値):

    oidcOptions.CallbackPath = new PathString("/signin-oidc");
    oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
    oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");
    
  • ("common" エンドポイントを使用する Microsoft Azure のみ) TokenValidationParameters.IssuerValidator: 多くの OIDC プロバイダーは既定の発行者検証コントロールで動作しますが、https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration によって返されるテナント ID ({TENANT ID}) でパラメーター化された発行者を考慮する必要があります。 詳細については、「OpenID Connect と Azure AD の "common" エンドポイントでの SecurityTokenInvalidIssuerException (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731)」を参照してください。

    Microsoft Entra ID または Azure AD B2C と "common" エンドポイントを使用するアプリの場合のみ:

    var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
    oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
    

サンプル アプリ コード

サンプル アプリで以下の機能を調べます。

  • カスタム cookie リフレッシャー (CookieOidcRefresher.cs) を使用する非対話形式でのトークンの自動更新。
  • PersistingAuthenticationStateProvider クラス (PersistingAuthenticationStateProvider.cs) は、PersistentComponentState を使用してクライアントに認証状態をフローするサーバー側 AuthenticationStateProvider であり、WebAssembly アプリケーションの有効期間中は固定されます。
  • Blazor Web アプリへの気象データの要求の例は、Program ファイル (Program.cs) の Minimal API エンドポイント (/weather-forecast) によって処理されます。 このエンドポイントは、RequireAuthorization を呼び出すことで認可を要求します。 プロジェクトに追加するすべてのコントローラーについて、[Authorize] 属性をコントローラーまたはアクションに追加します。
  • アプリは、気象データのサーバー プロジェクトで (Web) API を安全に呼び出します。
    • Weather コンポーネントをサーバーでレンダリングする場合、コンポーネントはサーバーで ServerWeatherForecaster を使用して気象データを直接取得します (Web API 呼び出しを使用しません)。
    • コンポーネントをクライアントでレンダリングする場合、コンポーネントは ClientWeatherForecaster サービス実装を使用します。この実装では、事前に構成した HttpClient (クライアント プロジェクトの Program ファイル内) を使用して、サーバー プロジェクトへの Web API 呼び出しが行われます。 サーバー プロジェクトの Program ファイルで定義されている最小 API エンドポイント (/weather-forecast) は、ServerWeatherForecaster から気象データを取得し、そのデータをクライアントに返します。

Blazor Web Apps のサービス抽象化を利用した (Web) API 呼び出しの詳細については、ASP.NET Core Blazor アプリからの Web API の呼び出しに関するページを参照してください。

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

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

PersistentAuthenticationStateProvider クラス (PersistentAuthenticationStateProvider.cs) は、ページがサーバーでレンダリングされたときにページに保持されていたデータを探すことでユーザーの認証状態を決定するクライアント側 AuthenticationStateProvider です。 認証状態は、WebAssembly アプリケーションの有効期間中は変わりません。

ユーザーがログインまたはログアウトする必要がある場合は、ページ全体の再読み込みが必要です。

サンプル アプリでは、表示のためにユーザー名とメールアドレスだけを提供します。 以降の要求を行うときにサーバーに対する認証を行うトークンは含まれません。これは、サーバーへの HttpClient 要求に含まれる cookie を使用して別に処理されます。

このバージョンの記事では、Backend for Frontend (BFF) パターンを使用して OIDC を実装する方法について説明します。 アプリの仕様上 BFF パターンを採用する必要がない場合は、記事のバージョン セレクターを BFF パターンを使用しない OIDC に変更してください。

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

  • Blazor Web アプリは、グローバル インタラクティビティを持つ自動レンダリング モードを使用します。
  • ユーザーの認証状態をキャプチャし、それをサーバーとクライアントの間でフローするために、サーバーおよびクライアント アプリによってカスタム認証状態プロバイダー サービスが使用されます。
  • このアプリは、すべての OIDC 認証フローの開始点です。 OIDC は、アプリ内で手動により構成され、Microsoft Entra IDMicrosoft Identity Web パッケージに依存せず、サンプル アプリも Microsoft Azure ホスティングを必要としません。 ただし、サンプル アプリは Entra や Microsoft Identity Web で使用でき、Azure でホストできます。
  • 非対話形式でのトークンの自動更新。
  • Backend for Frontend (BFF) パターンは、サービス検出のために .NET Aspire を使用し、要求をバックエンド アプリの天気予報エンドポイントにプロキシするために YARP を使用することで実現されます。
    • バックエンド Web API は、JWT ベアラー認証を使用して、Blazor Web アプリによりサインイン cookie に保存された JWT トークンを検証します。
    • Aspire は、.NET クラウドネイティブ アプリを構築するエクスペリエンスを強化します。 これは、分散型アプリを構築して実行するための、一貫性のある、厳格なツールとパターンのセットを提供します。
    • YARP (Yet Another Reverse Proxy) は、リバース プロキシ サーバーの作成に使用されるライブラリです。

プレビュー パッケージに関する警告

警告

BlazorWebAppOidcBff サンプル アプリで使用され、この記事で説明されているテクノロジとパッケージは、現時点では "プレビュー" リリース段階です。 この記事の内容、API、サンプル アプリは現時点ではサポートされておらず、現状では運用環境での使用は推奨されません。 サンプル アプリとガイダンスは、予告なく変更される場合があります。

前提条件

.NET Aspire には、Visual Studio バージョン 17.10 以降が必要です。

サンプル アプリ

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

  • .NET Aspire:
    • Aspire.AppHost: アプリの高レベルのオーケストレーションに関することを管理するために使用されます。
    • Aspire.ServiceDefaults: 必要に応じて拡張してカスタマイズできる .NET Aspire の既定のアプリ構成が含まれます。
  • MinimalApiJwt: 気象データ用の Minimal API エンドポイントの例を含むバックエンド Web API。
  • BlazorWebAppOidc: Blazor Web アプリのサーバー側プロジェクト。
  • BlazorWebAppOidc.Client: Blazor Web アプリのクライアント側プロジェクト。

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

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

.NET Aspire プロジェクト

.NET Aspire の使用方法の詳細と、サンプル アプリの .AppHost.ServiceDefaults プロジェクトの詳細については、.NET Aspire のドキュメントを参照してください。

.NET Aspire の前提条件を満たしていることを確認します。 詳細については、「クイックスタート: 初めての .NET Aspire アプリを構築する」の "前提条件" セクションを参照してください。

サーバー側 Blazor Web アプリ プロジェクト (BlazorWebAppOidc)

BlazorWebAppOidc プロジェクトは、Blazor Web アプリのサーバー側プロジェクトです。 このプロジェクトでは、バックエンド Web API プロジェクト (MinimalApiJwt) の天気予報エンドポイントに要求をプロキシするための YARP と、認証 cookie 内に保存された access_token を使用します。

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

Note

サーバー プロジェクトは IHttpContextAccessor/HttpContext を使いますが、対話形式でレンダリングされるコンポーネントには使いません。 詳細については、対話型サーバー側の ASP.NET Core Blazor レンダリングの脅威軽減策に関するガイダンスに関する記事を参照してください。

構成

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

Note

Microsoft Entra ID と Azure AD B2C の場合は、Microsoft Identity Web (Microsoft.Identity.Web NuGet パッケージAPI ドキュメント) の AddMicrosoftIdentityWebApp を使用できます。これにより、適切な既定値で OIDC ハンドラーと Cookie 認証ハンドラーの両方が追加されます。 このセクションのサンプル アプリとガイダンスでは、Microsoft Identity Web は使用しません。 このガイダンスでは、任意の OIDC プロバイダー用の OIDC ハンドラーを "手動で" 構成する方法を示します。 Microsoft Identity Web の実装の詳細については、リンクされているリソースを参照してください。

次の OpenIdConnectOptions の構成は、プロジェクトの Program ファイルに含まれる AddOpenIdConnect の呼び出しにあります。

  • SignInScheme: 認証の成功後に、ユーザーの ID の保持を担当するミドルウェアに対応する認証スキームを設定します。 OIDC ハンドラーでは、複数の要求にわたってユーザー資格情報を保持できるサインイン スキームを使用する必要があります。 次の行は、デモンストレーションの目的のためだけに存在します。 省略すると、DefaultSignInScheme がフォールバック値として使用されます。

    oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
  • openidprofile のスコープ (Scope) (省略可能): openidprofile スコープも、OIDC ハンドラーが機能するために必要なので既定で構成されますが、スコープが Authentication:Schemes:MicrosoftOidc:Scope の構成に含まれている場合は、これらを追加し直すことが必要になる場合があります。 一般的な構成のガイダンスについては、「ASP.NET Core の構成」と「ASP.NET Core Blazor の構成」を参照してください。

    oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
    
  • SaveTokens: 認可が成功した後で、アクセス トークンと更新トークンを AuthenticationProperties 内に保存する必要があるかどうかを定義します。 この値は、バックエンド Web API プロジェクト (MinimalApiJwt) からの気象データの要求を認証するために true に設定されます。

    oidcOptions.SaveTokens = true;
    
  • オフライン アクセスのスコープ (Scope): offline_access スコープが更新トークンのために必要です。

    oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
    
  • Web API から気象データを取得するためのスコープ (Scope): Weather.Get スコープは、Azure portal または Entra ポータルの [APIの公開] で構成します。 これは、バックエンド Web API プロジェクト (MinimalApiJwt) がベアラー JWT でアクセス トークンを検証するために必要です。

    oidcOptions.Scope.Add("{APP ID URI}/{API NAME}");
    

    例:

    • アプリ ID URI ({APP ID URI}): https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
      • ディレクトリ名 ({DIRECTORY NAME}): contoso
      • アプリケーション (クライアント) ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    • MinimalApiJwt ({API NAME}) からの気象データ用に構成されたスコープ: Weather.Get
    oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f/Weather.Get");
    

    前の例は、種類が AAD B2C テナントであるテナントに登録されたアプリに関するものです。 アプリが ME-ID テナントに登録されている場合、アプリ ID URI が異なるため、スコープも異なります。

    例:

    • アプリ ID URI ({APP ID URI}): api://{CLIENT ID}、アプリケーション (クライアント) ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    • MinimalApiJwt ({API NAME}) からの気象データ用に構成されたスコープ: Weather.Get
    oidcOptions.Scope.Add("api://4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f/Weather.Get");
    
  • AuthorityClientId: OIDC 呼び出しのためのオーソリティとクライアント ID を設定します。

    oidcOptions.Authority = "{AUTHORITY}";
    oidcOptions.ClientId = "{CLIENT ID}";
    

    例:

    • オーソリティ ({AUTHORITY}): https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/ (テナント ID a3942615-d115-4eb7-bc84-9974abcf5064 を使用)
    • クライアント ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    oidcOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    oidcOptions.ClientId = "4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    Microsoft Azure の "common" オーソリティの例:

    "common" オーソリティは、マルチテナント アプリに対して使用する必要があります。 "common" オーソリティをシングルテナント アプリに対して使用することもできますが、このセクションで後ほど示すように、カスタムの IssuerValidator が必要になります。

    oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
    
  • ClientSecret: OIDC クライアント シークレット。

    次の例は、テストとデモンストレーションの目的のためだけのものです。 クライアント シークレットをアプリのアセンブリ内に保存したり、シークレットをソース管理にチェックインしたりしないでください。 クライアント シークレットは、ユーザー シークレットAzure Key Vault、または環境変数の中に保存します。

    認証スキームの構成は、builder.Configuration["Authentication:Schemes:{SCHEME NAME}:{PropertyName}"] から自動的に読み取られます。ここでプレースホルダーの {SCHEME NAME} はスキームであり、既定では MicrosoftOidc です。 構成は事前に構成されているため、クライアント シークレットは、Authentication:Schemes:MicrosoftOidc:ClientSecret 構成キーを介して自動的に読み取ることができます。 環境変数を使用するサーバーでは、以下のように環境変数に Authentication__Schemes__MicrosoftOidc__ClientSecret という名前を付けます。

    set Authentication__Schemes__MicrosoftOidc__ClientSecret={CLIENT SECRET}
    

    デモンストレーションとテストに限りClientSecret を直接設定することができます。 デプロイされた運用アプリでは、値を直接設定しないでください。 若干のセキュリティの向上のためには、以下のように DEBUG シンボルを使用してこの行を条件付きでコンパイルします

    #if DEBUG
    oidcOptions.ClientSecret = "{CLIENT SECRET}";
    #endif
    

    例:

    クライアント シークレット ({CLIENT SECRET}): 463471c8c4...f90d674bc9 (表示用に短縮されています)

    #if DEBUG
    oidcOptions.ClientSecret = "463471c8c4...137f90d674bc9";
    #endif
    
  • ResponseType: 認可コード フローのみを実行するように、OIDC ハンドラーを構成します。 このモードでは、暗黙的な許可とハイブリッド フローは必要ありません。

    Entra または Azure portal の暗黙的な許可とハイブリッド フローのアプリ登録構成では、アクセス トークンまたは ID トークンを返すための承認エンドポイントのチェックボックスはどちらも選択*しないでください。 OIDC ハンドラーは、承認エンドポイントから返されたコードを使用して適切なトークンを自動的に要求します。

    oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
    
  • MapInboundClaims および NameClaimTypeRoleClaimType の構成: 多くの OIDC サーバーは、ClaimTypes の既定値 SOAP/WS-Fed ではなく、"name" と "role" を使用します。 MapInboundClaimsfalse に設定されていると、ハンドラーは要求のマッピングを実行せず、JWT からの要求名がアプリによって直接使用されます。 次の例では、名前とロールの要求を手動でマップします。

Note

ほとんどの OIDC プロバイダーでは、MapInboundClaimsfalse に設定する必要があります。こうすることで、要求の名前は変更されなくなります。

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
oidcOptions.TokenValidationParameters.RoleClaimType = "role";
  • パスの構成: パスは、OIDC プロバイダーにアプリケーションを登録するときに構成された、リダイレクト URI (ログイン コールバック パス) およびログアウト後リダイレクト (サインアウト コールバック パス) パスと、一致している必要があります。 Azure portal では、パスはアプリの登録の [認証] ブレードで構成されます。 サインインとサインアウト両方のパスを、リダイレクト URI として登録する必要があります。 既定値は、/signin-oidc/signout-callback-oidc です。

    • CallbackPath: ユーザーエージェントが返される、アプリのベース パス内の要求パス。

      Entra または Azure portal で、以下のように Web プラットフォーム構成のリダイレクト URI 内のパスを設定します。

      https://localhost/signin-oidc

      Note

      localhost アドレスにはポートは必要ありません。

    • SignedOutCallbackPath: ID プロバイダーからサインアウトした後でユーザー エージェントが返される、アプリのベース パス内の要求パス。

      Entra または Azure portal で、以下のように Web プラットフォーム構成のリダイレクト URI 内のパスを設定します。

      https://localhost/signout-callback-oidc

      Note

      localhost アドレスにはポートは必要ありません。

      Note

      Microsoft Identity Web を使用していて、microsoftonline.com オーソリティ (https://login.microsoftonline.com/{TENANT ID}/v2.0/) が使用されている場合、プロバイダーは現在、SignedOutCallbackPath にのみリダイレクトを行います。 Microsoft Identity Web で "common" オーソリティを使用できる場合は、この制限は存在しません。 詳細については、「オーソリティ URL にテナント ID が含まれていると postLogoutRedirectUri が機能しない (AzureAD/microsoft-authentication-library-for-js #5783)」を参照してください。

    • RemoteSignOutPath: このパスで要求を受け取ると、ハンドラーはサインアウト スキームを使用してサインアウトを呼び出します。

      Entra または Azure portal で、以下のようにフロントチャネル ログアウト URL を設定します。

      https://localhost/signout-oidc

      Note

      localhost アドレスにはポートは必要ありません。

    oidcOptions.CallbackPath = new PathString("{PATH}");
    oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
    oidcOptions.RemoteSignOutPath = new PathString("{PATH}");
    

    例 (既定値):

    oidcOptions.CallbackPath = new PathString("/signin-oidc");
    oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
    oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");
    
  • ("common" エンドポイントを使用する Microsoft Azure のみ) TokenValidationParameters.IssuerValidator: 多くの OIDC プロバイダーは既定の発行者検証コントロールで動作しますが、https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration によって返されるテナント ID ({TENANT ID}) でパラメーター化された発行者を考慮する必要があります。 詳細については、「OpenID Connect と Azure AD の "common" エンドポイントでの SecurityTokenInvalidIssuerException (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731)」を参照してください。

    Microsoft Entra ID または Azure AD B2C と "common" エンドポイントを使用するアプリの場合のみ:

    var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
    oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
    

サンプル アプリ コード

サンプル アプリで以下の機能を調べます。

  • カスタム cookie リフレッシャー (CookieOidcRefresher.cs) を使用する非対話形式でのトークンの自動更新。
  • PersistingAuthenticationStateProvider クラス (PersistingAuthenticationStateProvider.cs) は、PersistentComponentState を使用してクライアントに認証状態をフローするサーバー側 AuthenticationStateProvider であり、WebAssembly アプリケーションの有効期間中は固定されます。
  • Blazor Web アプリへの要求は、バックエンド Web API プロジェクト (MinimalApiJwt) にプロキシされます。 Program ファイル内の MapForwarder は、発信要求の既定の構成、カスタマイズされた変換、既定の HTTP クライアントを使用して、指定されたパターンに一致する HTTP 要求の特定の宛先への直接転送を追加します。
    • Weather コンポーネントをサーバーでレンダリングする場合、コンポーネントは ServerWeatherForecaster を使用し、ユーザーのアクセス トークンで気象データの要求をプロキシします。
    • コンポーネントをクライアントでレンダリングする場合、コンポーネントは ClientWeatherForecaster サービス実装を使用します。この実装では、事前に構成した HttpClient (クライアント プロジェクトの Program ファイル内) を使用して、サーバー プロジェクトへの Web API 呼び出しが行われます。 サーバー プロジェクトの Program ファイルで定義されている最小 API エンドポイント (/weather-forecast) は、ユーザーのアクセス トークンで要求を変換し、気象データを取得します。

Blazor Web Apps のサービス抽象化を利用した (Web) API 呼び出しの詳細については、ASP.NET Core Blazor アプリからの Web API の呼び出しに関するページを参照してください。

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

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

PersistentAuthenticationStateProvider クラス (PersistentAuthenticationStateProvider.cs) は、ページがサーバーでレンダリングされたときにページに保持されていたデータを探すことでユーザーの認証状態を決定するクライアント側 AuthenticationStateProvider です。 認証状態は、WebAssembly アプリケーションの有効期間中は変わりません。

ユーザーがログインまたはログアウトする必要がある場合は、ページ全体の再読み込みが必要です。

サンプル アプリでは、表示のためにユーザー名とメールアドレスだけを提供します。 以降の要求を行うときにサーバーに対する認証を行うトークンは含まれません。これは、サーバーへの HttpClient 要求に含まれる cookie を使用して別に処理されます。

バックエンド Web API プロジェクト (MinimalApiJwt)

MinimalApiJwt プロジェクトは、複数のフロントエンド プロジェクト用のバックエンド Web API です。 このプロジェクトでは、気象データ用の Minimal API エンドポイントを構成します。 Blazor Web アプリ サーバー側プロジェクト (BlazorWebAppOidc) からの要求は、MinimalApiJwt プロジェクトにプロキシされます。

構成

プロジェクトの Program ファイルに含まれる AddJwtBearer 呼び出しの JwtBearerOptions で以下のようにプロジェクトを構成します。

  • Audience: 受け取った OpenID Connect トークンに対して対象ユーザーを設定します。

    Azure portal または Entra ポータル: 以下のように [API の公開]Weather.Get スコープを追加するときに構成されたアプリケーション ID URI のパスに値を一致させます。

    jwtOptions.Audience = "{APP ID URI}";
    

    例:

    アプリ ID URI ({APP ID URI}): https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}:

    • ディレクトリ名 ({DIRECTORY NAME}): contoso
    • アプリケーション (クライアント) ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    jwtOptions.Audience = "https://contoso.onmicrosoft.com/4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    前の例は、種類が AAD B2C テナントであるテナントに登録されたアプリに関するものです。 アプリが ME-ID テナントに登録されている場合、アプリ ID URI が異なるため、対象ユーザーも異なります。

    例:

    アプリ ID URI ({APP ID URI}): api://{CLIENT ID}、アプリケーション (クライアント) ID ({CLIENT ID}): 4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f

    jwtOptions.Audience = "api://4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    
  • Authority: OpenID Connect の呼び出しを行うためのオーソリティを設定します。 以下のように BlazorWebAppOidc/Program.cs で OIDC ハンドラーに対して構成されているオーソリティに値を一致させます。

    jwtOptions.Authority = "{AUTHORITY}";
    

    例:

    オーソリティ ({AUTHORITY}): https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/ (テナント ID a3942615-d115-4eb7-bc84-9974abcf5064 を使用)

    jwtOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    

    前の例は、種類が AAD B2C テナントであるテナントに登録されたアプリに関するものです。 アプリが ME-ID テナントに登録されている場合、証明機関は ID プロバイダーから返された JWT の発行者 (iss) と一致する必要があります。

    jwtOptions.Authority = "https://sts.windows.net/a3942615-d115-4eb7-bc84-9974abcf5064/";
    

気象データ用の Minimal API

以下のようにプロジェクトの Program ファイル内の天気予報データ エンドポイントをセキュリティで保護します。

app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();

RequireAuthorization 拡張メソッドでは、ルート定義の認可が必要です。 プロジェクトに追加するすべてのコントローラーについて、[Authorize] 属性をコントローラーまたはアクションに追加します。

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

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

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

次の例では、LogInOrOut コンポーネントに対する重要な変更が示されています。 ReturnUrl の非表示フィールドの value がホーム ページである / に設定されています。 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 />
                <input type="hidden" name="ReturnUrl" value="/" />
                <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>

暗号化 nonce

nonce は、リプレイ攻撃を軽減するためにクライアントのセッションを ID トークンに関連付ける文字列値です。

認証の開発とテスト中に nonce エラーが発生した場合は、古い cookie データによって nonce エラーが発生する可能性があるため、アプリまたはテスト ユーザーに加えられた変更がどれだけ小さくても、テスト実行ごとに新しい InPrivate/incognito ブラウザー セッションを使用します。 詳細については、「Cookie とサイト データ」セクションを参照してください。

更新トークンが新しいアクセス トークンと交換されるときには、nonce が要求されたり使用されることはありません。 サンプル アプリでは、CookieOidcRefresher (CookieOidcRefresher.cs) が意図的に OpenIdConnectProtocolValidator.RequireNoncefalse に設定しています。

トラブルシューティング

ログ機能

サーバー アプリは標準の 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 アプリのテストとトラブルシューティングを行うときは、サーバー プロジェクトからアプリを実行していることを確認してください。

ユーザーを検査する

次の 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.Count() > 0)
{
    <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;
    }
}

その他のリソース