注
これは、この記事の最新バージョンではありません。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。
この記事では、Blazor アプリにトークンを渡す方法など、追加のセキュリティ シナリオのためにサーバー側の Blazor を構成する方法について説明します。
注
この記事のコード例では、null 許容参照型 (NRT) と .NET コンパイラの null 状態スタティック分析を採用しています。これは、.NET 6 以降の ASP.NET Core でサポートされています。 .NET 5 以前を対象とする場合は、記事の例の?、string?、TodoItem[]?、およびWeatherForecast[]?型から null 型の指定 (IEnumerable<GitHubBranch>?) を削除します。
サーバー側の Blazor アプリにトークンを渡す
このセクションは、 Blazor Web Appに適用されます。 Blazor Serverについては、この記事セクションの .NET 7 バージョンを参照してください。
アクセス トークンを使用してBlazor Web Appを使用してから Web API 呼び出しを行うだけの場合は、「Web API 呼び出しにトークン ハンドラーを使用する」セクションを参照してください。このセクションでは、DelegatingHandler実装を使用してユーザーのアクセス トークンを送信要求にアタッチする方法について説明しています。 このセクションの次のガイダンスは、他の目的でサーバー側でアクセス トークン、更新トークン、およびその他の認証プロパティを必要とする開発者を対象としています。
注
DelegatingHandler インスタンスの詳細については、「IHttpClientFactory を使用した HTTP 要求 - ASP.NET Core」を参照してください。
Blazor Web Appでサーバー側で使用するためにトークンやその他の認証プロパティを保存するには、IHttpContextAccessor/HttpContext (IHttpContextAccessor、HttpContext) を使用することをお勧めします。 静的サーバーサイドレンダリング(静的SSR)またはプリレンダリング中にトークンが取得されている場合、HttpContextを使用してからトークンを読み取り、IHttpContextAccessorとして取得することが、対話型サーバーレンダリング中に使用するためにサポートされています。 ただし、HttpContext接続の開始時にSignalRがキャプチャされるため、回線の確立後にユーザーが認証した場合、トークンは更新されません。 また、AsyncLocal<T>によるIHttpContextAccessorの使用は、HttpContextを読む前に実行コンテキストを失わないように注意する必要があることを意味します。 詳細については、Blazorを参照してください。
サービス クラスで、名前空間Microsoft.AspNetCore.Authenticationのメンバーへのアクセスを取得して、GetTokenAsyncのHttpContext メソッドを表示します。 次の例でコメントアウトされている別の方法は、AuthenticateAsyncでHttpContextを呼び出す方法です。 返された AuthenticateResult.Propertiesの場合は、 GetTokenValueを呼び出します。
using Microsoft.AspNetCore.Authentication;
public class AuthenticationProcessor(IHttpContextAccessor httpContextAccessor)
{
public async Task<string?> GetAccessToken()
{
if (httpContextAccessor.HttpContext is null)
{
throw new Exception("HttpContext not available");
}
// Approach 1: Call 'GetTokenAsync'
var accessToken = await httpContextAccessor.HttpContext
.GetTokenAsync("access_token");
// Approach 2: Authenticate the user and call 'GetTokenValue'
/*
var authResult = await httpContextAccessor.HttpContext.AuthenticateAsync();
var accessToken = authResult?.Properties?.GetTokenValue("access_token");
*/
return accessToken;
}
}
サービスは、サーバー プロジェクトの Program ファイルに登録されます。
builder.Services.AddScoped<AuthenticationProcessor>();
サーバー側のサービスにAuthenticationProcessorを挿入できます。たとえば、事前構成済みのDelegatingHandlerのためのHttpClientに挿入することができます。 次の例は、デモンストレーションのみを目的としています。または、 AuthenticationProcessor サービスで特別な処理を実行する必要がある場合は、 IHttpContextAccessor を挿入し、外部 Web API を呼び出すためのトークンを直接取得できます ( IHttpContextAccessor を直接使用して Web API を呼び出す方法の詳細については、「 Web API 呼び出しにトークン ハンドラーを使用 する」セクションを参照してください)。
using System.Net.Http.Headers;
public class TokenHandler(AuthenticationProcessor authProcessor) :
DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var accessToken = authProcessor.GetAccessToken();
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}
トークン ハンドラーが登録され、 Program ファイル内の名前付き HTTP クライアントの委任ハンドラーとして機能します。
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
throw new Exception("Missing base address!")))
.AddHttpMessageHandler<TokenHandler>();
注意事項
対話型自動レンダリングを採用し、クライアントまたはクライアント側のサービスによってレンダリングされるコンポーネントなど、トークンがクライアント ( .Client プロジェクト) によって送信および処理されないようにします。 クライアントが常にサーバー (プロジェクト) を呼び出して、トークンを使用して要求を処理するようにします。
トークンやその他の認証データがサーバーから離れることはありません。
対話型の自動コンポーネントについては、「 ASP.NET Core Blazor 認証と承認」を参照してください。これは、アクセス トークンやその他の認証プロパティをサーバーに残す方法を示しています。 また、同様の呼び出し構造を採用するバックエンド フロントエンド (BFF) パターンの採用を検討してください。これは、OIDC プロバイダー向けの OpenID Connect (OIDC) を使用した ASP.NET Core Blazor Web Appのセキュリティ保護と、Microsoft Blazor Web App Web 用 のセキュリティ保護に関するページで説明されています。
Web API 呼び出しにトークン ハンドラーを使用する
次のアプローチは、ユーザーのアクセス トークンを送信要求にアタッチすることを目的としています。特に、外部 Web API アプリへの Web API 呼び出しを行います。 このアプローチは、グローバル Interactive Server レンダリングを採用する Blazor Web App に対して示されていますが、グローバル対話型自動レンダリング モードを採用する Blazor Web Appにも同じ一般的なアプローチが適用されます。 留意すべき重要な概念は、HttpContextを使用してIHttpContextAccessorにアクセスすることは、サーバー上でのみ実行されるということです。
このセクションのガイダンスのデモについては、BlazorWebAppOidcのサンプル アプリ (.NET 8 以降) のBlazorWebAppOidcServerとBlazorを参照してください。 サンプルでは、Entra 固有のパッケージを使用せずに、Microsoft Entra でグローバル対話型レンダリング モードと OIDC 認証を採用しています。 サンプルでは、セキュリティで保護された Web API を呼び出すために JWT アクセス トークンを渡す方法を示します。
Microsoft Entra ID 用の Microsoft Identity Web パッケージを使用する Microsoft ID プラットフォームには、トークンの自動管理と更新を使用してBlazor Web Appから Web API を呼び出す API が用意されています。 詳細については、Blazor Web Appサンプル GitHub リポジトリのBlazorWebAppEntraする」とBlazorWebAppEntraBffおよびBlazorサンプル アプリ (.NET 9 以降) を参照してください。
ユーザーのアクセス トークンを送信要求にアタッチするサブクラス DelegatingHandler 。 トークン ハンドラーはサーバー上でのみ実行されるため、 HttpContext を使用しても安全です。
TokenHandler.cs:
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (httpContextAccessor.HttpContext is null)
{
throw new Exception("HttpContext not available");
}
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (accessToken is null)
{
throw new Exception("No access token");
}
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}
注
AuthenticationStateProviderからDelegatingHandlerを取得する方法については、「送信リクエストミドルウェア内でのAuthenticationStateProviderへのアクセス」セクションを参照してください。
プロジェクトのProgram ファイルでは、トークン ハンドラー (TokenHandler) がスコープ付きサービスとして登録され、メッセージ ハンドラーとして指定されます。
次の例では、 {HTTP CLIENT NAME} プレースホルダーは HttpClientの名前であり、 {BASE ADDRESS} プレースホルダーは Web API のベース アドレス URI です。
AddHttpContextAccessorの詳細については、「ASP.NET Core Blazor アプリの IHttpContextAccessor/HttpContext」を参照してください。
Program.cs:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("{HTTP CLIENT NAME}",
client => client.BaseAddress = new Uri("{BASE ADDRESS}"))
.AddHttpMessageHandler<TokenHandler>();
例:
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri("https://localhost:7277"))
.AddHttpMessageHandler<TokenHandler>();
を使用してbuilder.Configuration["{CONFIGURATION KEY}"]から HTTP クライアントのベース アドレスを指定できます。ここで、{CONFIGURATION KEY}プレースホルダーは構成キーです。
new Uri(builder.Configuration["ExternalApiUri"] ?? throw new IOException("No URI!"))
appsettings.jsonで、ExternalApiUriを指定します。 次の例では、外部 Web API の localhost アドレスにhttps://localhost:7277として値を設定します。
"ExternalApiUri": "https://localhost:7277"
この時点で、コンポーネントによって作成された HttpClient は、セキュリティで保護された Web API 要求を行うことができます。 次の例では、 {REQUEST URI} は相対要求 URI であり、 {HTTP CLIENT NAME} プレースホルダーは HttpClientの名前です。
using var request = new HttpRequestMessage(HttpMethod.Get, "{REQUEST URI}");
var client = ClientFactory.CreateClient("{HTTP CLIENT NAME}");
using var response = await client.SendAsync(request);
例:
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
追加機能は Blazor に対して計画されており、送信リクエストミドルウェア (AuthenticationStateProvider #52379)dotnet/aspnetcoreAccess によって追跡されています。
対話型サーバー モード (dotnet/aspnetcore #52390) での HttpClient へのアクセス トークンの提供 に関する問題は、高度なユース ケースに役立つディスカッションと潜在的な回避策を含むクローズされた問題です。
サーバー側の Razor アプリの Blazor コンポーネントの外部で使用できるトークンは、このセクションで説明する方法でコンポーネントに渡すことができます。 このセクションの例での焦点は、アクセス トークン、更新トークン、リクエスト フォージェリ防止 (XSRF) トークンを Blazor アプリに渡すことですが、このアプローチは他の HTTP コンテキスト状態に対しても有効です。
注
Razor コンポーネントに XSRF トークンを渡す処理は、コンポーネントが Identity や検証を必要とするその他のエンドポイントに POST を行うシナリオで役立ちます。 アプリに必要なのがアクセス トークンと更新トークンのみである場合は、以下の例から XSRF トークンのコードを削除できます。
通常の Razor Pages または MVC アプリと同様に、アプリを認証します。 トークンをプロビジョニングし、認証 cookie に保存します。
Program ファイルでは:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
builder.Services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
Startup.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
Startup.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
必要に応じて、options.Scope.Add("{SCOPE}"); を使用して、さらにスコープを追加します。ここで、プレースホルダー {SCOPE} は追加するスコープです。
依存関係の挿入 (DI) からトークンを解決するために Blazor アプリ内で使用できるスコープを持つトークン プロバイダー サービスを定義します。
TokenProvider.cs:
public class TokenProvider
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
Program ファイルで、次のサービスを追加します。
-
IHttpClientFactory: アクセス トークンを使ってサーバー API から気象データを取得する
WeatherForecastServiceクラスで使われます。 -
TokenProvider: アクセス トークンと更新トークンを保持します。
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();
Startup.ConfigureServices の Startup.cs で、次のサービスを追加します。
-
IHttpClientFactory: アクセス トークンを使ってサーバー API から気象データを取得する
WeatherForecastServiceクラスで使われます。 -
TokenProvider: アクセス トークンと更新トークンを保持します。
services.AddHttpClient();
services.AddScoped<TokenProvider>();
アクセス トークンと更新トークンを使って最初のアプリの状態を渡すクラスを定義します。
InitialApplicationState.cs:
public class InitialApplicationState
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
Pages/_Host.cshtml ファイルで、InitialApplicationState のインスタンスを作成し、それをパラメーターとしてアプリに渡します。
Pages/_Layout.cshtml ファイルで、InitialApplicationState のインスタンスを作成し、それをパラメーターとしてアプリに渡します。
Pages/_Host.cshtml ファイルで、InitialApplicationState のインスタンスを作成し、それをパラメーターとしてアプリに渡します。
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
...
@{
var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
};
}
<component ... param-InitialState="tokens" ... />
App コンポーネント (App.razor) で、サービスを解決し、パラメーターからのデータを使用してそれを初期化します。
@inject TokenProvider TokenProvider
...
@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }
protected override Task OnInitializedAsync()
{
TokenProvider.AccessToken = InitialState?.AccessToken;
TokenProvider.RefreshToken = InitialState?.RefreshToken;
TokenProvider.XsrfToken = InitialState?.XsrfToken;
return base.OnInitializedAsync();
}
}
注
前の例で TokenProvider に初期状態を割り当てる代わりに、OnInitializedAsync 内でスコープ サービスにデータをコピーしてアプリ全体で使用できます。
Microsoft.AspNet.WebApi.Client NuGet パッケージのパッケージ参照をアプリに追加します。
注
.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。
セキュリティで保護された API 要求を行うサービスで、トークン プロバイダーを挿入し、API 要求のトークンを取得します。
WeatherForecastService.cs:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class WeatherForecastService
{
private readonly HttpClient http;
private readonly TokenProvider tokenProvider;
public WeatherForecastService(IHttpClientFactory clientFactory,
TokenProvider tokenProvider)
{
http = clientFactory.CreateClient();
this.tokenProvider = tokenProvider;
}
public async Task<WeatherForecast[]> GetForecastAsync()
{
var token = tokenProvider.AccessToken;
using var request = new HttpRequestMessage(HttpMethod.Get,
"https://localhost:5003/WeatherForecast");
request.Headers.Add("Authorization", $"Bearer {token}");
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
Array.Empty<WeatherForecast>();
}
}
コンポーネントに渡される XSRF トークンに対して、TokenProvider を挿入し、POST 要求に XSRF トークンを追加します。 次の例では、ログアウト エンドポイントの POST にトークンを追加します。 次の例のシナリオでは、ログアウト エンドポイント (Areas/Identity/Pages/Account/Logout.cshtml、アプリにスキャフォールディングされている) で IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) が指定されていません。保護が必要な通常のログアウト操作に加えて、何らかのアクションが実行されるためです。 エンドポイントでは、要求を正常に処理するために有効な XSRF トークンが必要です。
承認されたユーザーに [Logout] ボタンを表示するコンポーネント:
@inject TokenProvider TokenProvider
...
<AuthorizeView>
<Authorized>
<form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
<button class="nav-link btn btn-link" type="submit">Logout</button>
<input name="__RequestVerificationToken" type="hidden"
value="@TokenProvider.XsrfToken">
</form>
</Authorized>
<NotAuthorized>
...
</NotAuthorized>
</AuthorizeView>
認証スキームを設定する
複数の認証ミドルウェアを使用し、複数の認証スキームを持つアプリの場合、 Blazor 使用するスキームは、 Program ファイルのエンドポイント構成で明示的に設定できます。 次の例では、OpenID Connect (OIDC) スキームを設定します。
複数の認証ミドルウェアを使用し、複数の認証スキームを持つアプリの場合、 Blazor 使用するスキームは、 Startup.csのエンドポイント構成で明示的に設定できます。 次の例では、OpenID Connect (OIDC) スキームを設定します。
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapRazorComponents<App>().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
})
.AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
});
複数の認証ミドルウェアを使用し、複数の認証スキームを持つアプリの場合、 Blazor 使用するスキームは、 Startup.Configureのエンドポイント構成で明示的に設定できます。 次の例では、Microsoft Entra ID スキームを設定します。
endpoints.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
});
OpenID Connect (OIDC) v2.0 エンドポイントを使用する
.NET 5 より前のバージョンの ASP.NET Core では、認証ライブラリと Blazor テンプレートで OpenID Connect (OIDC) v1.0 エンドポイントが使用されます。 .NET 5 より前のバージョンの ASP.NET Core で v2.0 エンドポイントを使用するには、OpenIdConnectOptions.Authorityで OpenIdConnectOptions オプションを構成します。
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
options =>
{
options.Authority += "/v2.0";
}
または、アプリ設定ファイル (appsettings.json) で設定を行うこともできます。
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common/oauth2/v2.0",
...
}
}
証明機関へのセグメントの追加がアプリの OIDC プロバイダー (ME-ID 以外のプロバイダーなど) にとって適さない場合は、Authority プロパティを直接設定します。 OpenIdConnectOptions またはアプリ設定ファイルで Authority キーを使用してプロパティを設定します。
コード変更
ID トークンの要求のリストは、v2.0 エンドポイントで変更されています。 これらの変更に関する Microsoft ドキュメントは廃止されましたが、ID トークン内の要求に関するガイダンスは、「ID トークンの要求のリファレンス」の中で参照することができます。
リソースは v2.0 エンドポイントのスコープ URI で指定されているため、OpenIdConnectOptions.Resource の OpenIdConnectOptions プロパティ設定を削除します。
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => { ... options.Resource = "..."; // REMOVE THIS LINE ... }
アプリ ID URI
- v2.0 エンドポイントを使用するとき、API により
App ID URIが定義されます。これは API の一意の識別子を表すものです。 - すべてのスコープにはプレフィックスとしてアプリ ID URI が含まれています。v2.0 エンドポイントからはアプリ ID URI を対象ユーザーとするアクセス トークンが発行されます。
- v2.0 エンドポイントを使用するとき、Server API で構成されたクライアント ID は API アプリケーション ID (クライアント ID) からアプリ ID URI に変更されます。
appsettings.json:
{
"AzureAd": {
...
"ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
...
}
}
使用するアプリ ID URI は、OIDC プロバイダーのアプリ登録の説明で見つけることができます。
カスタム サービスのユーザーをキャプチャするための回線ハンドラー
CircuitHandler からユーザーをキャプチャして、サービスでそのユーザーを設定するには、AuthenticationStateProvider を使います。 ユーザーを更新する場合は、AuthenticationStateChanged にコールバックを登録し、Task をエンキューして新しいユーザーを取得し、サービスを更新します。 このアプローチの例を次に示します。
次に例を示します。
- 回線が再接続されるたびに OnConnectionUpAsync が呼び出されて、接続の有効期間がユーザーに設定されます。 認証変更用のハンドラーを使って更新を実装しない場合は (次の例では OnConnectionUpAsync)、
AuthenticationChangedメソッドのみが必要です。 -
OnCircuitOpenedAsync が呼び出されて、ユーザーを更新するための認証変更ハンドラー
AuthenticationChangedがアタッチされます。 - コード実行のこの時点では例外を報告する方法がないため、
catchタスクのUpdateAuthenticationブロックは何も行いません。 タスクから例外がスローされた場合、例外はアプリ内の別の場所で報告されます。
UserService.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new(new ClaimsIdentity());
public ClaimsPrincipal GetUser() => currentUser;
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
: CircuitHandler, IDisposable
{
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());
public ClaimsPrincipal GetUser()
{
return currentUser;
}
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly UserService userService;
public UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
{
this.authenticationStateProvider = authenticationStateProvider;
this.userService = userService;
}
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
Program ファイルでは:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
Startup.ConfigureServices の Startup.cs で:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
services.AddScoped<UserService>();
services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
コンポーネントでサービスを使って、ユーザーを取得します。
@inject UserService UserService
<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>
MVC、Razor Pages、およびその他の ASP.NET Coreシナリオのミドルウェアでユーザーを設定するには、認証ミドルウェアの実行後にカスタム ミドルウェアのSetUserでUserServiceを呼び出すか、IClaimsTransformation実装を使用してユーザーを設定します。 次の例では、ミドルウェアの方法を使っています。
UserServiceMiddleware.cs:
public class UserServiceMiddleware
{
private readonly RequestDelegate next;
public UserServiceMiddleware(RequestDelegate next)
{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task InvokeAsync(HttpContext context, UserService service)
{
service.SetUser(context.User);
await next(context);
}
}
app.MapRazorComponents<App>() ファイルで Program を呼び出す直前に、ミドルウェアを呼び出します。
app.MapBlazorHub() ファイルで Program を呼び出す直前に、ミドルウェアを呼び出します。
app.MapBlazorHub() の Startup.Configure で Startup.cs を呼び出す直前に、ミドルウェアを呼び出します。
app.UseMiddleware<UserServiceMiddleware>();
送信要求ミドルウェアで AuthenticationStateProvider にアクセスする
IHttpClientFactory は DelegatingHandler インスタンスをアプリとは別の依存関係挿入 (DI) スコープで作成します。 派生AuthenticationStateProvider型にDelegatingHandlerを挿入した場合、ハンドラーはBlazor回線から現在のユーザーの認証状態にアクセスできません。
このシナリオに対処するには、次のいずれかの方法を使用します。
注
HttpClient を使用して作成された IHttpClientFactory インスタンスによる HTTP 要求の委任ハンドラーの定義に関する一般的なガイダンスについては、IHttpClientFactory を使用した HTTP 要求 - ASP.NET Coreの次のセクションを参照してください。
次のサブセクションの例では、認証されたユーザーのカスタム ユーザー名ヘッダーを送信要求にアタッチします。
アプリケーション スコープ ハンドラー (推奨)
このセクションのアプローチでは、キー付きサービスを使用して、ベース クライアントを HttpClient にアクセスする現在のアプリケーション スコープから解決されたアプリケーション スコープ ハンドラーにラップするカスタムAuthenticationStateProviderを登録します。
アプローチの概要:
- 基本クライアント構成: AddHttpClientに名前付きクライアントを登録するためにIHttpClientFactoryが呼び出されます。
- キー付き登録: カスタム
AddApplicationScopeHandler拡張メソッドは、キー付き HttpClient を同じクライアント名で登録します。 - スコープ対応ハンドラー: アプリケーション スコープ ハンドラーは現在のスコープから解決され、 AuthenticationStateProviderにアクセスできます。
- ハンドラー のキャッシュ: アプリケーション スコープ ハンドラーは、 IHttpMessageHandlerFactory を使用してキャッシュされた HttpMessageHandlerを取得し、接続プールを保持します。
- 構成の再利用: アプリケーション スコープ ハンドラーは、基本クライアントと同じ HttpClientFactoryOptions 構成を HttpClient に適用します。
次のメソッドとクラスを作成します。
-
AddApplicationScopeHandler: アプリケーション スコープ ハンドラーとキー付き HttpClient サービスを DI コンテナーに追加する拡張メソッド。 -
ApplicationScopeHandler: アプリケーション スコープ ハンドラー クラス。 -
AuthenticationStateHandler: 認証済みユーザーのカスタムユーザー名ヘッダーを送信されるリクエストに付加する DelegatingHandler 。
Services/ApplicationScopeHttpClientExtensions.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
namespace BlazorSample.Services;
public static class ApplicationScopeHttpClientExtensions
{
public static readonly HttpRequestOptionsKey<IServiceProvider> ScopeKey =
new("ApplicationScope");
public static IHttpClientBuilder AddApplicationScopeHandler(
this IHttpClientBuilder builder)
{
var name = builder.Name;
builder.Services.AddTransient<ApplicationScopeHandler>();
builder.Services.AddKeyedScoped<HttpClient>(name, (sp, key) =>
{
var handler = sp.GetRequiredService<ApplicationScopeHandler>();
handler.InnerHandler =
sp.GetRequiredService<IHttpMessageHandlerFactory>()
.CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);
var options =
sp.GetRequiredService<IOptionsMonitor<HttpClientFactoryOptions>>()
.Get(name);
foreach (var action in options.HttpClientActions)
{
action(client);
}
return client;
});
return builder;
}
}
public class ApplicationScopeHandler(IServiceProvider serviceProvider)
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Options.Set(ApplicationScopeHttpClientExtensions.ScopeKey,
serviceProvider);
return base.SendAsync(request, cancellationToken);
}
}
public class AuthenticationStateHandler : DelegatingHandler
{
private ClaimsPrincipal? user;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (user is null)
{
if (request.Options.TryGetValue(
ApplicationScopeHttpClientExtensions.ScopeKey, out var sp))
{
var authStateProvider = sp.GetService<AuthenticationStateProvider>();
if (authStateProvider is not null)
{
user = (await authStateProvider.GetAuthenticationStateAsync())
.User;
}
}
}
if (user?.Identity?.IsAuthenticated)
{
request.Headers.TryAddWithoutValidation("X-USER-IDENTITY-NAME",
user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
前の例の AuthenticationStateHandler は、 DelegatingHandlerの有効期間中にユーザーをキャッシュします。 要求ごとにユーザーの現在の認証状態をフェッチするには、ユーザーの null 条件付きチェックを削除します。
Program ファイルに名前付きクライアントを登録し、AddApplicationScopeHandlerを呼び出してアプリケーション スコープ ハンドラーを追加します。
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("{REQUEST URI}");
})
.AddApplicationScopeHandler()
.AddHttpMessageHandler<AuthenticationStateHandler>();
前の例の {REQUEST URI} プレースホルダーは、要求 URI です (localhost の例: http://localhost:5209)。
キー付きサービスを使用して、クライアントをコンポーネントに挿入します。
@using Microsoft.Extensions.DependencyInjection
@code {
[Inject(Key = "ExternalApi")]
public HttpClient Http { get; set; } = default!;
private async Task CallApiAsync()
{
var response = await Http.GetAsync("/api/endpoint");
}
}
回路アクティビティ処理装置
このセクションのアプローチでは、 回線アクティビティ ハンドラー を使用して AuthenticationStateProviderにアクセスします。これは、前のセクションで推奨される アプリケーション スコープ ハンドラーアプローチ の代替手段です。
まず、CircuitServicesAccessor 依存関係の挿入 (DI) に関する記事の以下のセクションで Blazor クラスを実装します。
異なる DI スコープからサーバー側の Blazor サービスにアクセスする
CircuitServicesAccessor を使用して、AuthenticationStateProvider 実装の DelegatingHandler にアクセスします。
AuthenticationStateHandler.cs:
using Microsoft.AspNetCore.Components.Authorization;
public class AuthenticationStateHandler(
CircuitServicesAccessor circuitServicesAccessor)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var authStateProvider = circuitServicesAccessor.Services?
.GetRequiredService<AuthenticationStateProvider>();
if (authStateProvider is null)
{
throw new Exception("AuthenticationStateProvider not available");
}
var authState = await authStateProvider.GetAuthenticationStateAsync();
var user = authState?.User;
if (user?.Identity is not null && user.Identity.IsAuthenticated)
{
request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
Program ファイルで、AuthenticationStateHandler を登録し、IHttpClientFactory インスタンスを作成する HttpClient にハンドラーを追加します。
builder.Services.AddTransient<AuthenticationStateHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<AuthenticationStateHandler>();
オペーク(リファレンス)アクセストークン対応
このセクションのガイダンスでは、JSON Web トークン (JWT) よりも次の利点を提供する、不透明 (参照) アクセス トークンのサポートを実装する方法について説明します。
- 厳密な失効: アクセス トークンが自然に期限切れになる前に、いつでも無効にします。
- トークン サイズの制限: 非常に大きな JWTを回避するために、多数のユーザー要求をトークンに格納します。
- セキュリティ: API コンシューマーまたはサード パーティがアクセス トークン要求を読み取らないようにします。
注
次のガイダンスでは、不透明 (参照) アクセス トークンをサポートする認証サーバーが必要です。 現在、Microsoft Entraでは、不透明なアクセス トークンの検証はサポートされていません。 Keycloak と Okta は、既定でアクセス トークン JWT 発行します。 このセクションの不透明なトークン ハンドラーは、RFC 7662 イントロスペクションにのみ依存するため、Keycloak と Okta に対して引き続き機能します。 このセクションの "不透明" では、クライアントがトークンを処理する方法ではなく、サーバーがトークンを作成する方法について説明します。 または、不透明なトークンのみを発行するように Duende IdentityServer を構成することもできます。
Keycloak に対してこのパターンをテストする場合、API のイントロスペクション クライアントは、ユーザーのアクセス トークンを発行した OIDC クライアントとは異なる OIDC クライアントである必要があります。 トークンを作成したクライアントを使用してトークンをイントロスペクトすると、サーバーのログに "{"active": false}" を含むAccess token JWT check failedが返されます。 これは、 Blazor Web App と最小 API (MinimalApiJwt) が個別のクライアントであるため、次のシナリオでは自然には発生しません。
AddOpenIdConnect では、Proof Key for Code Exchange (PKCE) 承認コード フロー用に構成されている場合、アクセス トークン検証が実行されないため、不透明なトークンがサポートされます。 ユーザーがサインイン後に ASP.NET Core アプリにリダイレクトしたときに受信した承認コードを使用して ID トークンを取得するには、OIDC 認証サービスへの ASP.NET Core サーバーの HTTPS バックチャネルに依存します。 有効な認証 cookieを取得するために OIDC を使用してユーザーをログインさせるだけでアプリが必要な場合は、アプリを変更せずに不透明なアクセス トークンがサポートされます。
エラーは、 AddOpenIdConnect によって取得された不透明なトークンが、 AddJwtBearerを使用して検証を試みる別のサービスに渡された場合にのみ発生します。 自己完結型 JWT とは異なり、不透明なトークンでは、状態を検証して要求を取得するために、承認サーバーへの要求が必要です。 この制限を回避するには、 Duende Introspection 認証ハンドラーなどのサードパーティ API を使用するか、トークンを検証する カスタム AuthenticationHandler を作成します。
Important
Duende Software および Okta は、Microsoftによって所有または制御されていないため、サービスとライブラリの運用環境での使用に対するライセンス料金の支払いを求められる場合があります。
次の AuthenticationHandler<TOptions> および関連する構成とヘルパー コードは、一般的なアプローチとして提供されています。これは、特定の承認サーバーの要件に合わせてさらに開発が必要になる場合があります。 次のハンドラーは、承認サーバーのイントロスペクション エンドポイントへの HTTP 呼び出しの Authorization ヘッダーから不透明なトークンを抽出し、ユーザーの要求を含む AuthenticationTicket を作成します。
承認サーバーのイントロスペクション エンドポイントを呼び出す場合は、認証が必要です。 次の例では、ローカルの開発とテストのために Secret Manager ツール を使用して、要求の Authorization ヘッダー (base64 でエンコードされた資格情報) で認証用のクライアント シークレットを設定します。
警告
アプリ シークレット、接続文字列、資格情報、パスワード、個人識別番号 (PIN)、プライベート C#/.NET コード、秘密キー/トークンをクライアント側コードに格納しないでください。これは安全ではありません。 テスト/ステージング環境と運用環境では、サーバー側の Blazor コードと Web API は、プロジェクト コードまたは構成ファイル内で資格情報を維持しないように、セキュリティで保護された認証フローを使用する必要があります。 ローカルの開発テスト以外では、環境変数は最も安全なアプローチとは言えないため、機密データを格納するのに環境変数を使用しないことをお勧めします。 ローカル開発テストでは、機密データをセキュリティで保護するために、 Secret Manager ツール をお勧めします。 詳細については、「 機密データと資格情報を安全に管理する」を参照してください。
次のハンドラーでは、承認サーバーのイントロスペクション エンドポイント クライアント シークレットが構成キー Authentication:Schemes:OpaqueTokenAuthentication:ClientSecretを使用します。 運用アプリの場合は、 クライアント アサーションの使用を検討してください。 詳細については、機密クライアント アサーション (Microsoft Entra ドキュメント)を参照してください。
Blazor サーバー プロジェクトが Secret Manager ツール用に初期化されていない場合は、Visual Studioの Developer PowerShell コマンド シェルなどのコマンド シェルを使用して、次のコマンドを実行します。 コマンドを実行する前に、 cd コマンドを使用してサーバー プロジェクトのディレクトリにディレクトリを変更します。 このコマンドは、ユーザー シークレット識別子 (サーバー アプリのプロジェクト ファイル内の<UserSecretsId> ) を確立します。
dotnet user-secrets init
次のコマンドを実行して、承認サーバーのクライアント シークレットを設定します。
{SECRET} プレースホルダーはクライアント シークレットです。
dotnet user-secrets set "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret" "{SECRET}"
Visual Studio を使用している場合は、 ソリューション エクスプローラー でサーバー プロジェクトを右クリックし、[ユーザー シークレットの管理] を選択することで 、シークレットが設定されていることを確認できます。
Extensions/HttpRequestExtensions.cs:
namespace MinimalApiJwt.Extensions;
public static class HttpRequestExtensions
{
public static string? ExtractBearerToken(this HttpRequest request)
{
var authorizationHeader = request.Headers.Authorization.ToString();
if (!string.IsNullOrEmpty(authorizationHeader) &&
authorizationHeader.StartsWith("Bearer ",
StringComparison.OrdinalIgnoreCase))
{
var token = authorizationHeader["Bearer ".Length..].Trim();
if (!string.IsNullOrEmpty(token))
{
return token;
}
}
return null;
}
}
Authentication/OpaqueTokenAuthenticationOptions.cs:
using Microsoft.AspNetCore.Authentication;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "OpaqueTokenAuthentication";
public string? IntrospectionEndpoint { get; set; }
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
}
次のハンドラーは、不透明 (参照) アクセス トークンの検証を試みます。 トークンと API の資格情報を使用して、承認サーバーのイントロスペクション エンドポイントに対して HTTP 呼び出しが行われます。 応答は、トークンが有効かどうかを判断するために処理されます。
- トークンが有効な場合は、ユーザーの要求を含む AuthenticationTicket が作成されます。
- トークンが無効な場合は、失敗した承認結果が返されます。
ハンドラーのオプション (Options) は、OpaqueTokenAuthenticationOptions基本型によって提供されるAuthenticationHandler<TOptions>のインスタンスです。これは、承認サーバーのイントロスペクション エンドポイントと API のクライアント ID を使用してアプリのProgram ファイルで構成されます。 API のクライアント シークレットは、開発中に Secret Manager ツールによって提供されます。
IOptionsMonitor<OpaqueTokenAuthenticationOptions> (optionsMonitor) はハンドラーによって直接使用されませんが、実行時に動的な構成変更をサポートするために使用できます。
FormUrlEncodedContent内の要求のコンテンツに対して、一部のサーバーにはトークンの種類のヒント (token_type_hint) が必要です。 たとえば、必要な値が access_token場合があります。 詳細については、認証サーバーのドキュメントを参照してください。
Authentication/OpaqueTokenAuthenticationHandler.cs:
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MinimalApiJwt.Extensions;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationHandler(
IOptionsMonitor<OpaqueTokenAuthenticationOptions> optionsMonitor,
ILoggerFactory logger,
UrlEncoder encoder,
IHttpClientFactory httpClientFactory)
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(optionsMonitor,
logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var opaqueToken = Request.ExtractBearerToken();
if (opaqueToken is null)
{
var failedResult = AuthenticateResult.Fail(
"Bearer token not found in Authorization header.");
return failedResult;
}
var introspectionUri = Options.IntrospectionEndpoint;
var clientId = Options.ClientId;
var clientSecret = Options.ClientSecret;
if (string.IsNullOrWhiteSpace(introspectionUri) ||
string.IsNullOrWhiteSpace(clientId) ||
string.IsNullOrWhiteSpace(clientSecret))
{
var failedResult = AuthenticateResult.Fail(
"Opaque token authentication isn't fully configured.");
return failedResult;
}
using var client = httpClientFactory.CreateClient();
// Set the Authorization header (base64 encoded credentials)
var authString = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authString);
// Prepare the form-encoded body containing the token
var content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("token", opaqueToken)
]);
// Post to the introspection endpoint
var response = await client.PostAsync(introspectionUri, content);
if (!response.IsSuccessStatusCode)
{
var failedResult = AuthenticateResult.Fail(
"Introspection endpoint failure.");
return failedResult;
}
// Parse the JSON response
var responseString = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseString);
// The 'active' property determines if the token is valid and not expired
var tokenIsValid =
doc.RootElement.TryGetProperty("active", out var activeProperty) &&
activeProperty.ValueKind == JsonValueKind.True;
if (tokenIsValid)
{
// Map standard introspection response fields onto claims.
// Field names below match what Keycloak, Duende IdentityServer,
// Auth0, and Okta return; adjust the role source for your provider.
var claims = new List<Claim>();
string? Get(string name) =>
doc.RootElement.TryGetProperty(name, out var v) &&
v.ValueKind == JsonValueKind.String ? v.GetString() : null;
var sub = Get("sub");
var username = Get("preferred_username") ?? Get("username") ?? sub;
if (sub is not null) claims.Add(new Claim(ClaimTypes.NameIdentifier, sub));
if (username is not null) claims.Add(new Claim(ClaimTypes.Name, username));
if (Get("email") is { } email) claims.Add(new Claim(ClaimTypes.Email, email));
if ((Get("client_id") ?? Get("azp")) is { } cid)
claims.Add(new Claim("client_id", cid));
if (Get("scope") is { } scope)
foreach (var s in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries))
claims.Add(new Claim("scope", s));
// Keycloak surfaces realm roles under realm_access.roles.
// Duende/IdentityServer uses a flat "role" claim; Auth0 uses a
// configurable custom claim. Adjust for your authorization server.
if (doc.RootElement.TryGetProperty("realm_access", out var ra) &&
ra.ValueKind == JsonValueKind.Object &&
ra.TryGetProperty("roles", out var roles) &&
roles.ValueKind == JsonValueKind.Array)
{
foreach (var r in roles.EnumerateArray())
if (r.ValueKind == JsonValueKind.String)
claims.Add(new Claim(ClaimTypes.Role, r.GetString()!));
}
var identity = new ClaimsIdentity(claims,
OpaqueTokenAuthenticationOptions.DefaultScheme,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal,
OpaqueTokenAuthenticationOptions.DefaultScheme);
var result = AuthenticateResult.Success(ticket);
return result;
}
else
{
var failedResult = AuthenticateResult.Fail("Bearer token invalid.");
return failedResult;
}
}
}
注
上記のアプローチは、OpenID Connect 検出エンドポイントを使用し、クライアントの HttpClient イントロスペクション要求のキャッシュを追加することで、さらに改善できます。
Program ファイルでは:
using MinimalApiJwt.Authentication;
...
builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
OpaqueTokenAuthenticationOptions.DefaultScheme,
options =>
{
options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
options.ClientId = "{API CLIENT ID}";
options.ClientSecret =
builder.Configuration[
"Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret"];
});
前の例のプレースホルダーは次のとおりです。
-
{AUTH SERVER INTROSPECTION URI}: 認証サーバーのイントロスペクション URI -
{API CLIENT ID}: API クライアント ID
認証サーバーのイントロスペクション URI ({AUTH SERVER INTROSPECTION URI}) と API クライアント ID ({API CLIENT ID}) の値は、アプリ設定またはその他の構成ソースから指定できます。
トークンは通常、失効エンドポイントを使用してログアウト イベントで無効になります。 次の例は、さらなる開発の出発点です。
app.MapPost("/logout",
async ([FromForm] string? returnUrl, HttpContext context,
IHttpClientFactory httpClientFactory) =>
{
var accessToken = await context.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(accessToken))
{
// Prepare the revocation request (RFC 7009)
var content =
new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "token", accessToken },
{ "token_type_hint", "access_token" },
{ "client_id", "{API CLIENT ID}" },
{ "client_secret", "{CLIENT SECRET}" }
});
// POST to the revocation endpoint
using var client = httpClientFactory.CreateClient();
await client.PostAsync("{AUTH SERVER TOKEN REVOCATION URI}", content);
}
return TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "{REDIRECT URI}" },
[CookieAuthenticationDefaults.AuthenticationScheme]);
});
前の例のプレースホルダーは次のとおりです。
-
{AUTH SERVER TOKEN REVOCATION URI}: 認証サーバーのトークン失効 URI。 -
{API CLIENT ID}: API クライアント ID。 -
{CLIENT SECRET}: 安全に取得されたクライアント シークレット。 -
{REDIRECT URI}: リダイレクト URI。
Duende IdentityServer では、CoordinateLifetimeWithUserSession クライアント構成プロパティを true に設定することで、トークンが自動的に取り消されます。このプロパティは、セッションの終了時に関連付けられているトークンを自動的にクリーンアップします。 詳細については、 セッションのクリーンアップとログアウト (Duende のドキュメント) を参照してください。
.NETの今後のリリースでは、組み込みの不透明なアクセス トークンのサポートが検討されています。 詳細については、「 不透明 - 参照トークンの検証 (dotnet/aspnetcore #46026)」を参照してください。
サーバー側の Blazor アプリ承認パターン
Blazor WebAssembly アプリに適用されるパターンについては、「セキュリティで保護された ASP.NET Core Blazor WebAssembly」を参照してください。
通常、サーバー側 Blazor アプリ (Blazor Web App、 Blazor Server アプリ) では、承認を要求するために次 のいずれかの 方法が採用されます。
- アプリは、認証されたユーザーを必要としないリソース (Razor コンポーネント、静的資産など) に
[AllowAnonymous]属性を適用して、アプリ全体で承認を必要とする承認フォールバック ポリシーを設定します。 詳細については、「 フォールバック承認ポリシーを使用したグローバル承認 」セクションを参照してください。 - アプリは、リソースのグローバル承認を要求する代わりに、承認されたユーザーを必要とするリソースに
[Authorize]属性 を適用します。 詳細については、「[Authorize]属性を使用したローカル承認」セクションを参照してください。
フォールバック承認ポリシーを使用したグローバル承認
次のデモ コードは、BlazorWebAppAuthorization サンプル アプリ (dotnet/AspNetCore.Docs.Samples GitHub リポジトリ) と共に使用できます (ダウンロード方法)。
AuthorizationOptions.FallbackPolicyを RequireAuthenticatedUser を持つポリシーに設定します。これは、特定のリソースに対して承認属性または明示的なポリシーが設定されていない場合にのみ適用されます。
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
フレームワークの AuthorizationOptions.DefaultPolicy には、認証されたユーザーが必要です。 アプリがカスタムの既定のポリシーを持つ カスタム ポリシー プロバイダー を使用しない限り、前の例に示すようにフレームワークの既定のポリシー (options.DefaultPolicy) を割り当てることは、次のコードを使用することと同じです。
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
アプリでは、特定のポリシーが設定されていないリソースに対して認証されたユーザーが必要です。
アプリのセキュリティ仕様で静的資産の保護が呼び出されない場合は、MapStaticAssetsでAuthorizationEndpointConventionBuilderExtensions.AllowAnonymousを呼び出します。
app.MapStaticAssets().AllowAnonymous();
または、特定のパスに対して匿名アクセスを許可するには、StaticAssetsEndpointConventionBuilder.Addのエンドポイント規則ラムダ内のルート パターンにAllowAnonymousAttributeを適用します。
Important
匿名アクセスに対して特定のエンドポイントのみを承認する場合は、 Blazor スクリプト とその他の Blazor 静的アセット (スタイルシート、スクリプト、モジュールなど) を考慮する必要があります。 パブリック Razor コンポーネント ページでアセットを正しくレンダリングして機能させる必要がある場合、アセットは、静的アセットルーティング エンドポイント規則または静的ファイル ミドルウェアを介して個別に要求されるため、匿名で使用できるようにする必要があります。
匿名アクセス用の静的資産を 1 つのフォルダーに配置します。 次の例では、 /public/ パス セグメントを持つエンドポイント ルートが匿名で提供されます。
app.MapStaticAssets()
.Add(endpointBuilder =>
{
if (endpointBuilder is RouteEndpointBuilder routeBuilder &&
routeBuilder.RoutePattern.RawText?.Contains(
"/public/", StringComparison.OrdinalIgnoreCase) == true)
{
routeBuilder.Metadata.Add(new AllowAnonymousAttribute());
}
});
次の例では、圧縮されていない Blazor スクリプト (_framework/blazor.web.{FINGERPRINT}.js、 {FINGERPRINT} プレースホルダーはファイルのフィンガープリント) を匿名で提供する方法を示します。
// using System.Text.RegularExpressions;
var regex = new Regex(
@"^_framework/blazor\.web\.[a-z0-9]{10}\.js$", RegexOptions.Compiled);
app.MapStaticAssets()
.Add(endpointBuilder =>
{
if (endpointBuilder is RouteEndpointBuilder routeBuilder &&
regex.IsMatch(routeBuilder.RoutePattern.RawText ?? string.Empty))
{
routeBuilder.Metadata.Add(new AllowAnonymousAttribute());
}
});
アプリのセキュリティ仕様で静的資産の保護が呼び出されない場合は、UseStaticFiles前の呼び出しを行いUseAuthenticationUseAuthorizationします。
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
または、特定のパスに対して匿名アクセスを許可するには、 UseAuthentication と UseAuthorization が呼び出される前に、個別の静的ファイル ミドルウェアを登録します。 承認パイプライン処理後に UseStaticFiles する 2 回目の呼び出しでは、ユーザーが承認されている場合にのみ、他の静的資産が処理されます。
Important
匿名アクセスに対して特定のエンドポイントのみを承認する場合は、 Blazor スクリプト とその他の Blazor 静的アセット (スタイルシート、スクリプト、モジュールなど) を考慮する必要があります。 パブリック Razor コンポーネント ページでアセットを正しくレンダリングして機能させる必要がある場合、アセットは静的ファイル ミドルウェアを介して個別に要求されるため、匿名で使用できるようにする必要があります。
次の例では、アプリの wwwroot/public フォルダー内の静的アセットが匿名で提供されます。
app.UseStaticFiles(new StaticFileOptions {
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(
System.IO.Path.Combine(builder.Environment.WebRootPath, "public")),
RequestPath = "/public"
});
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
個々のコンポーネントへの匿名アクセスを許可するには、@attribute 名前空間には [AllowAnonymous] ディレクティブを使用し、@using には Microsoft.AspNetCore.Authorization ディレクティブを使用します。 次の例では、 Home コンポーネントによって属性が設定されます。
Components/Pages/Home.razorの上部:
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [AllowAnonymous]
多くの場合、コンポーネントのフォルダー全体に承認を適用すると便利です。 次の例では、ユーザー アカウント ページのインポート ファイルによって [AllowAnonymous] 属性が設定されるため、ユーザーは Components/Account/Pages フォルダー内のアプリのサインイン、サインアウト、アクセス拒否、および無効なユーザー ページに匿名でアクセスできます。
Components/Account/Pages/_Imports.razor:
@using Microsoft.AspNetCore.Authorization
@attribute [AllowAnonymous]
アプリが 1 つ以上のエンドポイント規則ビルダー インスタンスを使用して、 Identity コンポーネントなどの追加のエンドポイントを提供する場合、エンドポイント ビルダーのメソッド呼び出しは、 AuthorizationEndpointConventionBuilderExtensions.AllowAnonymousへの呼び出しをチェーンします。 次の例では、IEndpointConventionBuilderを返す MapAdditionalIdentityEndpoints を呼び出すことによって、追加のIdentity エンドポイントをマップします。
app.MapAdditionalIdentityEndpoints().AllowAnonymous();
注
上記のMapAdditionalIdentityEndpointsメソッドの例については、BlazorWebAppAuthorization サンプル アプリ (dotnet/AspNetCore.Docs.Samples GitHub リポジトリ) のIdentityComponentsEndpointRouteBuilderExtensionsを参照してください。
[Authorize]属性を使用したローカル承認
コンポーネントに、次のRazorの方法で属性([Authorize])を適用します。
アプリの imports ファイルで、
@usingディレクティブとして、Microsoft.AspNetCore.Authorization 名称空間に対して@attributeディレクティブを追加し、[Authorize]属性のために ディレクティブを追加します。_Imports.razor:@using Microsoft.AspNetCore.Authorization @attribute [Authorize]インポート ファイルは、フォルダー階層の任意のレベルで適用して、そのフォルダーのコンポーネントとそのサブフォルダーに
[Authorize]属性を適用できます。Razor ディレクティブの下で承認を必要とする各
@pageコンポーネントに、 名前空間の[Authorize]ディレクティブを使用して@usingを追加します。@using Microsoft.AspNetCore.Authorization @attribute [Authorize]前の例のMicrosoft.AspNetCore.Authorization名前空間の
@usingディレクティブは、個々のコンポーネントではなく、アプリのインポート ファイル (_Imports.razor) に配置することで、アプリのコンポーネントに広く適用できます。
ASP.NET Core