サーバー側の ASP.NET Core Blazor の追加のセキュリティ シナリオ

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、Blazor アプリにトークンを渡す方法など、追加のセキュリティ シナリオのためにサーバー側の Blazor を構成する方法について説明します。

Note

この記事のコード例では、null 許容参照型 (NRT) と .NET コンパイラの null 状態スタティック分析を採用しています。これは、.NET 6 以降の ASP.NET Core でサポートされています。 ASP.NET Core 5.0 以前をターゲットとする場合は、記事の例の string?TodoItem[]?WeatherForecast[]?、および IEnumerable<GitHubBranch>? 型から null 型の指定 (?) を削除します。

サーバー側の Blazor アプリにトークンを渡す

Blazor Web Apps のこのセクションの更新は、Blazor Web Apps ( dotnet/AspNetCore.Docs #31691) でのトークンの受け渡しに関する更新セクションを保留中です。 詳細については、「対話型サーバー モードでの HttpClient へのアクセス トークンの提供に関する問題 ( dotnet/aspnetcore#52390)」を参照してください。

Blazor Server については、この記事セクションの 7.0 バージョンを参照してください。

サーバー側の Blazor アプリの Razor コンポーネントの外部で使用できるトークンは、このセクションで説明する方法でコンポーネントに渡すことができます。 このセクションの例での焦点は、アクセス トークン、更新トークン、リクエスト フォージェリ防止 (XSRF) トークンを Blazor アプリに渡すことですが、このアプローチは他の HTTP コンテキスト状態に対しても有効です。

Note

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.csStartup.ConfigureServices で、次のサービスを追加します。

  • 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 パッケージのパッケージ参照をアプリに追加します。

Note

.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;
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        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 エンドポイントを使用する

5\.0 より前のバージョンの ASP.NET Core では、認証ライブラリと Blazor テンプレートに OpenID Connect (OIDC) v1.0 エンドポイントが使用されます。 5\.0 より前のバージョンの ASP.NET Core で v2.0 エンドポイントを使用するには、OpenIdConnectOptionsOpenIdConnectOptions.Authority オプションを構成します。

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 で指定されているため、OpenIdConnectOptionsOpenIdConnectOptions.Resource プロパティ設定を削除します。

    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 プロバイダーのアプリ登録の説明で見つけることができます。

カスタム サービスのユーザーをキャプチャするための回線ハンドラー

AuthenticationStateProvider からユーザーをキャプチャして、サービスでそのユーザーを設定するには、CircuitHandler を使います。 ユーザーを更新する場合は、AuthenticationStateChanged にコールバックを登録し、Task をキューに入れて新しいユーザーを取得してサービスを更新します。 このアプローチの例を次に示します。

次に例を示します。

  • 回線が再接続されるたびに OnConnectionUpAsync が呼び出されて、接続の有効期間がユーザーに設定されます。 認証変更用のハンドラーを使って更新を実装しない場合は (次の例では AuthenticationChanged)、OnConnectionUpAsync メソッドのみが必要です。
  • OnCircuitOpenedAsync が呼び出されて、ユーザーを更新するための認証変更ハンドラー AuthenticationChanged がアタッチされます。
  • コード実行のこの時点では例外を報告する方法がないため、UpdateAuthentication タスクの catch ブロックは何も行いません。 タスクから例外がスローされた場合、例外はアプリ内の別の場所で報告されます。

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()
    {
        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;
    }
}
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.csStartup.ConfigureServices で:

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 のシナリオのミドルウェアでユーザーを設定するには、認証ミドルウェアの実行後にカスタム ミドルウェアで UserService に対して SetUser を呼び出すか、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);
    }
}

Program ファイルで app.MapRazorComponents<App>() を呼び出す直前に、ミドルウェアを呼び出します。

Program ファイルで app.MapBlazorHub() を呼び出す直前に、ミドルウェアを呼び出します。

Startup.csStartup.Configureapp.MapBlazorHub() を呼び出す直前に、ミドルウェアを呼び出します。

app.UseMiddleware<UserServiceMiddleware>();

送信要求ミドルウェアで AuthenticationStateProvider にアクセスする

IHttpClientFactory で作成された HttpClient 用の DelegatingHandler からの AuthenticationStateProvider には、回線アクティビティ ハンドラーを使用して送信リクエスト ミドルウェアでアクセスできます。

注意

ASP.NET Core アプリで IHttpClientFactory を使用して作成された HttpClient インスタンスごとに HTTP リクエスト用の委任ハンドラーを定義する方法の一般的なガイダンスについては、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」における以下のセクションを参照してください。

次の例では、AuthenticationStateProvider を使用して、認証済みユーザーのカスタム ユーザー名ヘッダーを送信要求に添付します。

まず、Blazor 依存関係の挿入 (DI) に関する記事の以下のセクションで CircuitServicesAccessor クラスを実装します。

異なる DI スコープからサーバー側の Blazor サービスにアクセスする

CircuitServicesAccessor を使用して、DelegatingHandler 実装の AuthenticationStateProvider にアクセスします。

AuthenticationStateHandler.cs:

public class AuthenticationStateHandler : DelegatingHandler
{
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public AuthenticationStateHandler(
        CircuitServicesAccessor circuitServicesAccessor)
    {
        this.circuitServicesAccessor = circuitServicesAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services
            .GetRequiredService<AuthenticationStateProvider>();
        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 を登録し、HttpClient インスタンスを作成する IHttpClientFactory にハンドラーを追加します。

builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();