ASP.NET Core Blazor WebAssembly のセキュリティに関するその他のシナリオ

注意

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

この記事では、Blazor WebAssembly アプリの追加セキュリティ シナリオについて説明します。

送信要求にトークンを添付する

AuthorizationMessageHandler は、アクセス トークンの処理に使用される DelegatingHandler です。 トークンは、フレームワークによって登録されている IAccessTokenProvider サービスを使用して取得されます。 トークンを取得できない場合は、AccessTokenNotAvailableException がスローされます。 AccessTokenNotAvailableException には、特定の AccessTokenResult.InteractionOptions を使用して AccessTokenResult.InteractiveRequestUrl に移動してアクセス トークンを更新できるようにする Redirect メソッドがあります。

便宜上、フレームワークには、アプリのベース アドレスを承認された URL として使用して事前構成が行われた BaseAddressAuthorizationMessageHandler が用意されています。 アクセス トークンは、要求 URI がアプリのベース URI 内にある場合にのみ追加されます。 送信要求 URI がアプリのベース URI 内にない場合は、カスタム AuthorizationMessageHandler クラス (推奨) を使用するか、または AuthorizationMessageHandler を構成します。

Note

サーバー API アクセス用のクライアント アプリ構成に加えて、クライアントとサーバーが同じベース アドレス内に存在しない場合にクロスオリジン要求 (CORS) がサーバー API によって許可される必要があります。 サーバー側の CORS 構成の詳細については、この記事で後述する「クロスオリジン リソース共有 (CORS)」セクションを参照してください。

次の例では

次の例で、HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http の拡張機能です。 パッケージを、まだ参照していないアプリに追加します。

メモ

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、要求 URI は、アプリのベース URI 内にあります。 したがって、IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) は、プロジェクト テンプレートから生成されたアプリ内の HttpClient.BaseAddress に割り当てられます。

構成された HttpClient を使用し、try-catch パターンを使用して、承認された要求を行います。

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}

カスタム認証要求のシナリオ

次のシナリオでは、認証要求をカスタマイズする方法と、認証オプションからログイン パスを取得する方法を示します。

ログイン プロセスをカスタマイズする

InteractiveRequestOptions の新しいインスタンスで次のメソッドを 1 回以上使用して、ログイン要求への追加パラメーターを管理します。

次の LoginDisplay コンポーネントの例では、ログイン要求にパラメーターが追加されています。

  • promptlogin に設定されています。ユーザーがその要求に対して資格情報を入力するように強制し、シングル サインオンを否定します。
  • loginHintpeter@contoso.com に設定されています。ユーザーのサインイン ページのユーザー名/メール アドレス フィールドを peter@contoso.com に事前入力します。 preferred_username クレームを使用して以前のサインインから既にユーザー名を抽出しているため、アプリでは再認証中にこのパラメーターを使用することがよくあります。

Shared/LoginDisplay.razor:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}

詳細については、次のリソースを参照してください。

トークンを対話的に取得する前にオプションをカスタマイズする

AccessTokenNotAvailableException が発生した場合は、InteractiveRequestOptions の新しいインスタンスで次のメソッドを 1 回以上使用して、新しい ID プロバイダーのアクセス トークン要求の追加パラメーターを管理します。

Web API を使用して JSON データを取得する次の例では、アクセス トークンが使用できない (AccessTokenNotAvailableException がスローされる) 場合に、追加のパラメーターがリダイレクト要求に追加されます。

  • promptlogin に設定されています。ユーザーがその要求に対して資格情報を入力するように強制し、シングル サインオンを否定します。
  • loginHintpeter@contoso.com に設定されています。ユーザーのサインイン ページのユーザー名/メール アドレス フィールドを peter@contoso.com に事前入力します。 preferred_username クレームを使用して以前のサインインから既にユーザー名を抽出しているため、アプリでは再認証中にこのパラメーターを使用することがよくあります。
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}

上記の例の場合、以下を前提としています。

詳細については、次のリソースを参照してください。

IAccessTokenProvider を使用する場合のオプションをカスタマイズする

IAccessTokenProvider の使用時にトークンを取得できない場合は、InteractiveRequestOptions の新しいインスタンスで次のメソッドを 1 回以上使用して、新しい ID プロバイダーのアクセス トークン要求の追加パラメーターを管理します。

ユーザーのアクセス トークンの取得を試みる次の例では、TryGetToken の呼び出し時にトークンを取得する試みが失敗した場合に、ログイン要求に追加のパラメーターが追加されます。

  • promptlogin に設定されています。ユーザーがその要求に対して資格情報を入力するように強制し、シングル サインオンを否定します。
  • loginHintpeter@contoso.com に設定されています。ユーザーのサインイン ページのユーザー名/メール アドレス フィールドを peter@contoso.com に事前入力します。 preferred_username クレームを使用して以前のサインインから既にユーザー名を抽出しているため、アプリでは再認証中にこのパラメーターを使用することがよくあります。
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}

上記の例の場合、以下を前提としています。

詳細については、次のリソースを参照してください。

カスタムの戻り先 URL を使用してログアウトする

次の例では、ユーザーをログアウトし、/goodbye エンドポイントにユーザーを返します。

Navigation.NavigateToLogout("authentication/logout", "goodbye");

認証オプションからログイン パスを取得する

RemoteAuthenticationOptions から構成されたログイン パスを取得します。

var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

上記の例の場合、以下を前提としています。

カスタム AuthorizationMessageHandler クラス

このセクションのこのガイダンスは、アプリのベース URI 内に存在しない URI に対して送信要求を行うクライアント アプリに推奨されます。

次の例では、カスタム クラスによって、AuthorizationMessageHandler が、HttpClient 用の DelegatingHandler として使用するために拡張されます。 ConfigureHandler によって、アクセス トークンを使用して送信 HTTP 要求を承認できるように、このハンドラーが構成されます。 アクセス トークンがアタッチされるのは、承認された URL の少なくとも 1 つが要求 URI (HttpRequestMessage.RequestUri) のベースである場合に限られます。

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
            scopes: new[] { "example.read", "example.write" });
    }
}

上記のコードのスコープ example.readexample.write は、特定のプロバイダーに有効なスコープを反映することを意図したものではなく、一般的な例です。

Program ファイルでは、CustomAuthorizationMessageHandler が一時的なサービスとして登録され、名前付き HttpClient によって作成された送信 HttpResponseMessage インスタンスの DelegatingHandler として構成されます。

次の例で、HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http の拡張機能です。 パッケージを、まだ参照していないアプリに追加します。

メモ

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

メモ

前の例では、CustomAuthorizationMessageHandlerDelegatingHandlerAddHttpMessageHandler の一時的なサービスとして登録されています。 独自の DI スコープを管理する IHttpClientFactory に、一時的な登録をお勧めします。 詳細については、次のリソースを参照してください。

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) が HttpClient.BaseAddress に割り当てられます。

構成が行われた HttpClient を使用し、try-catch パターンを使用して、承認された要求を行います。 CreateClient (Microsoft.Extensions.Http パッケージ) を使用してクライアントが作成される場合にサーバー API への要求を行うと、アクセス トークンが含まれるインスタンスが HttpClient に提供されます。 要求 URI が次の例 (ExampleAPIMethod) のように相対 URI である場合、それは、クライアント アプリから要求があったときに BaseAddress に結合されます。

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

AuthorizationMessageHandler を構成する

ConfigureHandler メソッドを使用すれば、承認された URL、スコープ、および戻り先 URL で AuthorizationMessageHandler を構成できます。 ConfigureHandler によって、アクセス トークンを使用して送信 HTTP 要求を承認できるように、ハンドラーが構成されます。 アクセス トークンがアタッチされるのは、承認された URL の少なくとも 1 つが要求 URI (HttpRequestMessage.RequestUri) のベースである場合に限られます。 要求 URI が相対 URI である場合、これは BaseAddress に結合されます。

次の例では、Program ファイル内で AuthorizationMessageHandler により HttpClient が構成されています。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }))
    {
        BaseAddress = new Uri("https://api.contoso.com/v1.0")
    });

上記のコードのスコープ example.readexample.write は、特定のプロバイダーに有効なスコープを反映することを意図したものではなく、一般的な例です。

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、IWebAssemblyHostEnvironment.BaseAddress が以下に割り当てられます。

型指定された HttpClient

単一クラス内のすべての HTTP およびトークンの取得に関する問題を処理する、型指定されたクライアントを定義できます。

WeatherForecastClient.cs:

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[] forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

前の例では、WeatherForecast 型は天気予報データを保持する静的クラスです。 プレースホルダー {ASSEMBLY NAME} は、アプリのアセンブリ名です (例: using static BlazorSample.Data;)。

次の例で、HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http の拡張機能です。 パッケージを、まだ参照していないアプリに追加します。

メモ

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

Program ファイルで次の操作を行います。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) が HttpClient.BaseAddress に割り当てられます。

気象データをフェッチするコンポーネントの場合。

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}

HttpClient ハンドラーを構成する

ハンドラーは、送信 HTTP 要求の ConfigureHandler を使用して、さらに構成を行うことができます。

次の例で、HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http の拡張機能です。 パッケージを、まだ参照していないアプリに追加します。

メモ

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

Program ファイルで次の操作を行います。

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new [] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));

上記のコードのスコープ example.readexample.write は、特定のプロバイダーに有効なスコープを反映することを意図したものではなく、一般的な例です。

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、IWebAssemblyHostEnvironment.BaseAddress が以下に割り当てられます。

セキュリティで保護された既定のクライアントを使用する、アプリ内の認証または承認されていない Web API 要求

通常はセキュリティで保護された既定の HttpClient を使うアプリで、名前付きの HttpClient を構成することで、未認証または未承認の Web API 要求を行うこともできます。

次の例で、HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http の拡張機能です。 パッケージを、まだ参照していないアプリに追加します。

メモ

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

Program ファイルで次の操作を行います。

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションの場合、既定では、IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) が HttpClient.BaseAddress に割り当てられます。

前述の登録は、セキュリティで保護された既定の HttpClient 登録に追加されます。

コンポーネントでは、IHttpClientFactory (Microsoft.Extensions.Http パッケージ) から HttpClient が作成され、認証または承認されていない要求が行われます。

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}

Note

サーバー API のコントローラー (前の例では ExampleNoAuthenticationController) では、[Authorize] 属性を使用してマークされることはありません。

既定の HttpClient インスタンスとしてセキュリティで保護されたクライアントを使用するか、セキュリティで保護されていないクライアントを使用するかは、開発者が決定します。 この決定を行う方法の 1 つは、アプリが通信する認証済みのエンドポイントと認証されていないエンドポイントの数を考慮することです。 アプリの要求の大部分が API エンドポイントをセキュリティで保護する場合は、認証された HttpClient インスタンスを既定として使用します。 それ以外の場合は、認証されていない HttpClient インスタンスを既定として登録します。

IHttpClientFactory を使用する別の方法として、匿名エンドポイントへの認証されていないアクセス用に、型指定されたクライアントを作成することもできます。

追加のアクセス トークンを要求する

アクセス トークンは、IAccessTokenProvider.RequestAccessToken を呼び出して手動で取得できます。 次の例では、既定の HttpClient に対して、アプリが追加のスコープを必要としています。 Microsoft Authentication Library (MSAL) の例では、MsalProviderOptions を使用してスコープが構成されます。

Program ファイルで次の操作を行います。

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}

前の例の {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} のプレースホルダーは、カスタム スコープです。

Note

ユーザーが最初に Microsoft Azure に登録されたアプリを使用する場合、Microsoft Entra ID 同意 UI を介して Microsoft Graph の委任されたユーザー アクセス許可を AdditionalScopesToConsent でプロビジョニングすることはできません。 詳細については、「ASP.NET Core Blazor WebAssembly での Graph API の使用」を参照してください。

IAccessTokenProvider.RequestAccessToken メソッドには、指定されたスコープ セットを使用して、アプリでアクセス トークンをプロビジョニングできるようにするオーバーロードが用意されています。

Razor コンポーネントでは、次のようになります。

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}

前の例の {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} のプレースホルダーは、カスタム スコープです。

AccessTokenResult.TryGetToken が次のように返します。

  • token を使用する場合は true
  • トークンが取得されない場合は false

クロスオリジン リソース共有 (CORS)

CORS 要求で資格情報 (承認 cookie またはヘッダー) を送信するときには、CORS ポリシーで Authorization ヘッダーが許可されている必要があります。

次のポリシーには、以下についての構成が含まれています。

  • 要求元 (http://localhost:5000https://localhost:5001)。
  • 任意のメソッド (動詞)。
  • Content-Type ヘッダーと Authorization ヘッダー。 カスタム ヘッダー (x-custom-headerなど) を許可するには、WithHeaders の呼び出し時にヘッダーを一覧表示します。
  • クライアント側の JavaScript コードによって設定された資格情報 (credentials プロパティが includeに設定されています)。
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Blazor WebAssembly プロジェクト テンプレートに基づいてホストされている Blazor ソリューションを使用すると、クライアント アプリとサーバー アプリに同じベース アドレスが使用されます。 クライアント アプリの HttpClient.BaseAddress は、既定では builder.HostEnvironment.BaseAddress の URI に設定されます。 ホストされている Blazor ソリューションの既定の構成では、CORS 構成は必須では "ありません"。 サーバー プロジェクトでホストされておらず、サーバー アプリのベース アドレスを共有していない追加のクライアント アプリでは、サーバー プロジェクト内の CORS 構成は "必須です"。

詳細については、「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」と、サンプル アプリの HTTP Request Tester コンポーネント (Components/HTTPRequestTester.razor) を参照してください。

トークン要求エラーを処理する

シングル ページ アプリケーション (SPA) で OpenID Connect (OIDC) を使用してユーザーが認証されると、ユーザーが資格情報を入力したときに設定されるセッション cookie の形式で、SPA 内および Identity プロバイダー (IP) 内で、認証状態がローカルに維持されます。

通常、IP によってユーザーに出力されるトークンが有効なのは短時間のため (通常は約 1 時間)、クライアント アプリでは定期的に新しいトークンをフェッチする必要があります。 それ以外の場合は、許可されたトークンの有効期限が切れると、ユーザーがログアウトします。 ほとんどの場合、OIDC クライアントでは、ユーザーに対して認証の再要求を行うことなく、新しいトークンをプロビジョニングできます。これは、認証状態または IP 内に保持される "セッション" によるものです。

場合によっては、ユーザーの介入なしに、クライアントでトークンを取得できないことがあります。たとえば、何らかの理由でユーザーが明示的に IP からログアウトした場合などです。 このシナリオは、ユーザーが https://login.microsoftonline.com にアクセスしてログアウトした場合に発生します。これらのシナリオでは、ユーザーがログアウトしたことを、アプリはすぐに認識しません。クライアントで保持されるトークンは、有効でなくなった可能性があります。 また、クライアントでは、現在のトークンの有効期限が切れた後に、ユーザーの介入なしに新しいトークンをプロビジョニングすることはできません。

これらのシナリオは、トークンベースの認証に固有のものではありません。 これらは、SPA の性質の一部です。 また、認証 cookie が削除されると、cookie を使用する SPA でサーバー API を呼び出すこともできません。

保護されたリソースに対する API 呼び出しをアプリで実行するときは、次の点に注意する必要があります。

  • API を呼び出すための新しいアクセス トークンをプロビジョニングするには、ユーザーの再認証が必要です。
  • 有効かもしれないトークンがクライアントにある場合でも、そのトークンはユーザーによって取り消されているため、サーバーへの呼び出しが失敗する可能性があります。

アプリでトークンが要求されている場合は、次の 2 つの結果が考えられます。

  • 要求が成功します。アプリには有効なトークンがあります。
  • 要求が失敗します。新しいトークンを取得するために、ユーザーの再認証が必要となります。

トークン要求が失敗した場合は、リダイレクトを実行する前に、現在の状態を保存するかどうかを決定する必要があります。 状態を保存する方法はいくつかありますが、さらに複雑になります。

  • 現在のページの状態をセッション ストレージに格納します。 OnInitializedAsync ライフサイクル メソッド (OnInitializedAsync) 中に、続行する前に状態を復元できるかどうかを確認します。
  • クエリ文字列パラメーターを追加して、以前に保存した状態を再ハイドレートする必要があることをアプリに通知する方法として使用します。
  • 他の項目と競合するリスクなしにセッション ストレージにデータを格納するための一意識別子を持つクエリ文字列パラメーターを追加します。

セッション ストレージを使って認証操作の前にアプリの状態を保存する

以下の例では、次のことを行っています。

  • ログイン ページにリダイレクトする前に、状態を保持します。
  • クエリ文字列パラメーターを使って、認証後に以前の状態を回復します。
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}

セッション ストレージと状態コンテナーを使って認証操作の前にアプリの状態を保存する

認証操作中に、ブラウザーが IP にリダイレクトされる前に、アプリの状態を保存することが必要になる場合があります。 状態コンテナーを使用していて、認証が成功した後に状態を復元する場合には、このようなことが起こる可能性があります。 カスタム認証状態オブジェクトを使用して、アプリ固有の状態、またはその参照を保持し、認証操作が正常に完了した後で、その状態を復元することができます。 このアプローチの例を次に示します。

状態コンテナー クラスは、アプリの状態値を保持するプロパティを使用して、アプリ内に作成されます。 次の例では、コンテナーを使用して、既定の Blazor プロジェクト テンプレートCounter コンポーネント (Counter.razor) のカウンター値を維持します。 コンテナーをシリアル化および逆シリアル化するためのメソッドは、System.Text.Json に基づいています。

using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage()
    {
        return JsonSerializer.Serialize(this);
    }

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}

Counter コンポーネントでは、状態コンテナーを使用して、コンポーネントの外部に currentCount 値を維持します。

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}

RemoteAuthenticationState から ApplicationAuthenticationState を作成します。 ローカルに格納されている状態の識別子として機能する Id プロパティを指定します。

ApplicationAuthenticationState.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string Id { get; set; }
}

Authentication コンポーネント (Authentication.razor) では、StateContainer のシリアル化と逆シリアル化の方法である GetStateForLocalStorage および SetStateFromLocalStorage で、ローカル セッション ストレージを使用してアプリの状態を保存および復元します。

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string? Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}

この例では、Microsoft Entra (ME-ID) を使用して認証を行います。 Program ファイルで次の操作を行います。

  • ApplicationAuthenticationState は、Microsoft Authentication Library (MSAL) の RemoteAuthenticationState 型として、構成が行われます。
  • 状態コンテナーがサービス コンテナーに登録されます。
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

アプリ ルートをカスタマイズする

Microsoft.AspNetCore.Components.WebAssembly.Authentication ライブラリでは既定で、次の表に示すルートを使用して、さまざまな認証状態が表されます。

ルート 目的
authentication/login サインイン操作をトリガーします。
authentication/login-callback サインイン操作の結果を処理します。
authentication/login-failed 何らかの理由でサインイン操作が失敗した場合に、エラー メッセージを表示します。
authentication/logout サインアウト操作をトリガーします。
authentication/logout-callback サインアウト操作の結果を処理します。
authentication/logout-failed 何らかの理由でサインアウト操作が失敗した場合に、エラー メッセージを表示します。
authentication/logged-out ユーザーが正常にログアウトしたことを示します。
authentication/profile ユーザー プロファイルを編集する操作をトリガーします。
authentication/register 新しいユーザーを登録する操作をトリガーします。

上の表に示すルートは、RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths を使用して構成できます。 カスタム ルートを提供するオプションを設定する場合は、アプリに各パスを処理するルートがあることを確認します。

次の例では、すべてのパスが /security で始まります。

Authentication コンポーネント (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}

Program ファイルで次の操作を行います。

builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

完全に異なるパスを必要とする場合は、前述のようにルートを設定し、明示的なアクション パラメーターを使用して RemoteAuthenticatorView をレンダリングします。

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

UI を別のページに分割することもできます。

認証ユーザー インターフェイスをカスタマイズする

RemoteAuthenticatorView には、各認証状態の UI フラグメントの既定のセットが含まれます。 各状態は、カスタム RenderFragment を渡すことでカスタマイズできます。 最初のログイン プロセス中に表示されるテキストをカスタマイズするには、次のように RemoteAuthenticatorView を変更します。

Authentication コンポーネント (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }
}

RemoteAuthenticatorView には、次の表に示す認証ルートごとに使用できるフラグメントが 1 つあります。

ルート Fragment
authentication/login <LoggingIn>
authentication/login-callback <CompletingLoggingIn>
authentication/login-failed <LogInFailed>
authentication/logout <LogOut>
authentication/logout-callback <CompletingLogOut>
authentication/logout-failed <LogOutFailed>
authentication/logged-out <LogOutSucceeded>
authentication/profile <UserProfile>
authentication/register <Registering>

ユーザーをカスタマイズする

アプリにバインドされているユーザーをカスタマイズできます。

ペイロード要求を使用してユーザーをカスタマイズする

次の例では、アプリの認証されたユーザーが、ユーザーの認証方法ごとに amr 要求を受け取ります。 この amr 要求は、Microsoft Identity プラットフォーム v1.0 ペイロード要求でトークンのサブジェクトが認証された方法を示しています。 この例では、RemoteUserAccount に基づくカスタム ユーザー アカウント クラスを使用します。

RemoteUserAccount クラスを拡張するクラスを作成します。 次の例では、AuthenticationMethod プロパティを amrJSON プロパティ値のユーザー配列に設定します。 ユーザーが認証されると、フレームワークによって AuthenticationMethod が自動的に設定されます。

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[] AuthenticationMethod { get; set; }
}

AccountClaimsPrincipalFactory<TAccount> を拡張するファクトリを作成して、CustomUserAccount.AuthenticationMethod に格納されているユーザーの認証方法から要求を作成します。

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            foreach (var value in account.AuthenticationMethod)
            {
                userIdentity.AddClaim(new Claim("amr", value));
            }
        }

        return initialUser;
    }
}

使用中の認証プロバイダーの CustomAccountFactory を登録します。 次の登録のいずれかが有効です。

  • AddOidcAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddMsalAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddApiAuthorization:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    

カスタム ユーザー アカウント クラスを持つ ME-ID セキュリティ グループおよびロール

ME-ID セキュリティ グループと ME-ID 管理者ロール、およびカスタム ユーザー アカウント クラスを使用するその他の例については、「Microsoft Entra ID のグループとロールを使用する ASP.NET Core Blazor WebAssembly」をご覧ください。

認証を使用したプリレンダリング

現在、認証と認可を必要とするプリレンダリング コンテンツはサポートされていません。 Blazor WebAssembly セキュリティ アプリのトピックのいずれかのガイダンスを実行した後は、この後の手順に従って次のようなアプリを作成できます。

  • 承認が不要なパスをプリレンダリングする。
  • 承認が必要なパスをプリレンダリングしない。

Client プロジェクトの Program ファイルは、共通のサービスの登録を別々のメソッドの中に分解 (たとえば、Client プロジェクトで ConfigureCommonServices メソッドを作成) します。 一般的なサービスとは、開発者が、クライアント プロジェクトとサーバー プロジェクトの両方で使用するために登録するものです。

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Program ファイルで次の操作を行います。

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

Server プロジェクトの Program ファイルで、次の追加サービスを登録し、ConfigureCommonServices を呼び出します。

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

Server プロジェクトの Startup.ConfigureServices メソッドで、次の追加サービスを登録し、ConfigureCommonServices を呼び出します。

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}

Blazor フレームワーク サーバー認証プロバイダー (ServerAuthenticationStateProvider) の詳細については、「ASP.NET Core Blazor の認証と承認」を参照してください。

Server プロジェクトの Pages/_Host.cshtml ファイルで、Component タグ ヘルパー (<component ... />) を次のコードに置き換えます。

<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>

前の例の場合:

  • プレースホルダー {CLIENT APP ASSEMBLY NAME} は、クライアント アプリのアセンブリ名です (例: BlazorSample.Client)。
  • /authentication パス セグメントの条件付きチェック:
    • 認証パスのプリレンダリング (render-mode="WebAssembly") を避けます。
    • 非認証パスをプリレンダリングします (render-mode="WebAssemblyPrerendered")。

ホストされているアプリおよびサードパーティ ログイン プロバイダーに関するオプション

ホストされている Blazor WebAssembly アプリをサードパーティ プロバイダーで認証および承認する場合、ユーザーの認証にはいくつかのオプションを使用できます。 どれを選択するかは、シナリオによって異なります。

詳しくは、「ASP.NET Core で外部プロバイダーからの追加の要求とトークンを保持する」をご覧ください。

ユーザーを認証して保護されたサードパーティ API のみを呼び出す

サードパーティ API プロバイダーに対してクライアント側の OAuth フローを使用してユーザーを認証します。

builder.services.AddOidcAuthentication(options => { ... });

このシナリオでは:

  • アプリをホストしているサーバーは関与しません。
  • サーバー上の API を保護することはできません。
  • アプリでは、保護されたサードパーティ API のみを呼び出すことができます。

サードパーティ プロバイダーでユーザーを認証し、ホスト サーバーおよびサード パーティ上で保護された API を呼び出す

サードパーティのログイン プロバイダーを使用して、Identity の構成を行います。 サードパーティ API へのアクセスに必要なトークンを取得し、それを格納します。

ユーザーがログインすると、認証プロセスの一環として、アクセス トークンと更新トークンが IdentityID によって収集されます。 その時点で、サードパーティ API の API 呼び出しを行うために使用できる方法はいくつかあります。

サーバー アクセス トークンを使用してサードパーティのアクセス トークンを取得する

サーバー上で生成されたアクセス トークンを使用して、サーバー API エンドポイントからサードパーティのアクセストークンを取得します。 そこから、サードパーティのアクセス トークンを使用して、クライアント上の IdentityID からサードパーティ API リソースを直接呼び出します。

"この方法はお勧めしません。" この方法では、サードパーティのアクセス トークンをパブリック クライアント用に生成されたものとして扱う必要があります。 OAuth 規約では、パブリック アプリにはクライアント シークレットがありません。これはシークレットを安全に格納することが信頼できないためです。アクセス トークンは機密クライアントに対して生成されます。 機密クライアントとは、クライアント シークレットを持っていてシークレットを安全に格納できると想定されるクライアントです。

  • サードパーティのアクセス トークンには、サードパーティがより信頼できるクライアントのトークンを生成したという事実に基づいて機密性の高い操作を実行するための追加のスコープが付与される場合があります。
  • 同様に、信頼されていないクライアントに更新トークンを発行してはなりません。それを行ってしまうと、他の制限が適用されない限り、クライアントは無制限にアクセスできます。

サードパーティ API を呼び出すために、クライアントからサーバー API への API 呼び出しを行う

クライアントからサーバー API への API 呼び出しを行います。 サーバーから、サードパーティ API リソースのアクセス トークンを取得し、必要な呼び出しはすべて発行します。

"この方法をお勧めします。" この方法では、サードパーティ API を呼び出すためにサーバー経由で追加のネットワーク ホップが必要になりますが、それによって最終的にはより安全なエクスペリエンスが得られます。

  • サーバーでは、更新トークンを格納し、アプリからサードパーティ リソースへのアクセスが決して失われないようにすることができます。
  • アプリでは、より機密性の高いアクセス許可を含む可能性のあるサーバーからのアクセス トークンをリークさせることはできません。

OpenID Connect (OIDC) v2.0 エンドポイントを使用する

認証ライブラリと Blazor プロジェクト テンプレートにより、OpenID Connect (OIDC) v1.0 エンドポイントが使用されます。 v2.0 エンドポイントを使用するには、JWT ベアラー JwtBearerOptions.Authority オプションの構成を行います。 次の例では、Authority プロパティに v2.0 セグメントを追加することで、v2.0 に対して ME-ID の構成が行われます。

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });

または、アプリ設定ファイル (appsettings.json) で設定を行うこともできます。

{
  "Local": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

証明機関へのセグメントを追跡することがアプリの OIDC プロバイダー (ME-ID 以外のプロバイダーなど) にとって適切でない場合は、Authority プロパティを直接設定します。 JwtBearerOptions またはアプリ設定ファイル (appsettings.json) で Authority キーを使用してプロパティを設定します。

ID トークンの要求のリストは、v2.0 エンドポイントで変更されています。 この変更に関する Microsoft ドキュメントは廃止されましたが、ID トークンの要求に関するガイダンスについては、「ID トークンの要求のリファレンス」を参照してください。

コンポーネントで gRPC を構成し、使用する

ASP.NET Core gRPC フレームワークを使用するように Blazor WebAssembly アプリを構成するには:

Note

Blazor Web Apps では既定でプリレンダリングが有効なため、最初にサーバーから、次にクライアントからレンダリングされるコンポーネントを考慮する必要があります。 プリレンダリングされた状態は、再利用できるようにクライアントに送信される必要があります。 詳細については、「ASP.NET Core Razor のプリレンダリングとコンポーネント」をご覧ください。

Note

Blazor WebAssembly アプリでは既定でプリレンダリングが有効なため、最初にサーバーから、次にクライアントからレンダリングされるコンポーネントを考慮する必要があります。 プリレンダリングされた状態は、再利用できるようにクライアントに送信される必要があります。 詳細については、「ASP.NET Core Razor コンポーネントのプリレンダリングと統合を行う」を参照してください。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});

クライアント アプリのコンポーネントでは、gRPC クライアント (Grpc.razor) を使用し、gRPC 呼び出しを行うことができます。

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}

Status.DebugException プロパティを使用するには、Grpc.Net.Client バージョン 2.30.0 以降を使用します。

詳細については、「ASP.NET Core gRPC アプリでの gRPC-Web」参照してください。

AuthenticationService の実装を置き換える

以下のサブセクションでは、置き換える方法について説明します。

  • 任意の JavaScript AuthenticationService の実装。
  • JavaScript 用 Microsoft 認証ライブラリ (MSAL.js)。

任意の JavaScript AuthenticationService の実装を置き換える

カスタム認証の詳細を処理する JavaScript ライブラリを作成します。

警告

このセクションのガイダンスは、既定の RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions> の実装の詳細です。 このセクションの TypeScript コードは、特に .NET 7 の ASP.NET Core 7 に適用されます。これは、ASP.NET Core の今後のリリースで予告なく変更される場合があります。

// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};

ライブラリをインポートするには、元の <script> タグを削除し、カスタム ライブラリを読み込む <script> タグを追加します。 次の例では、既定の <script> タグを、CustomAuthenticationService.js という名前のライブラリを wwwroot/js フォルダーから読み込むタグに置き換える方法を示します。

</body> 終了タグ内の Blazor スクリプト (_framework/blazor.webassembly.js) の前にある wwwroot/index.html の内容:

- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

詳細については、dotnet/aspnetcore GitHub リポジトリの AuthenticationService.ts を参照してください。

注意

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

JavaScript 用 Microsoft 認証ライブラリ (MSAL.js) を置き換える

アプリでカスタム バージョンの JavaScript 用 Microsoft 認証ライブラリ (MSAL.js) が必要な場合は、次の手順を実行します。

  1. システムに最新の開発者用 .NET SDK がインストールされていることを確認するか、次から最新の開発者用 SDK を入手してインストールします: .NET Core SDK: インストーラーとバイナリ。 このシナリオでは、内部 NuGet フィードの構成は必要ありません。
  2. ソースからの ASP.NET Core のビルドに関するページのドキュメントに従って、開発用の dotnet/aspnetcore GitHub リポジトリを設定します。 dotnet/aspnetcore GitHub リポジトリをフォークしてクローンするか、ZIP アーカイブをダウンロードします。
  3. src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json ファイルを開き、必要なバージョンの @azure/msal-browser を設定します。 リリース済みバージョンの一覧については、@azure/msal-browser の npm Web サイトにアクセスし、 [Versions](バージョン) タブを選択してください。
  4. コマンド シェルで yarn build コマンドを使用して、src/Components/WebAssembly/Authentication.Msal/src フォルダー内に Authentication.Msal プロジェクトをビルドします。
  5. アプリで圧縮された資産 (Brotli/Gzip) を使用する場合は、Interop/dist/Release/AuthenticationService.js ファイルを圧縮します。
  6. AuthenticationService.js ファイルと、生成した場合はファイルの圧縮バージョン (.br/.gz) を、Interop/dist/Release フォルダーから、アプリの発行済み資産にあるアプリの publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal フォルダーにコピーします。

カスタム プロバイダー オプションを渡す

基になる JavaScript ライブラリにデータを渡すためのクラスを定義します。

重要

このクラスの構造は、System.Text.Json を使用して JSON がシリアル化されるときにライブラリで想定されている構造と一致する必要があります。

次の例は、仮想的なカスタム プロバイダー ライブラリの想定に一致する JsonPropertyName 属性を持つ ProviderOptions クラスを示しています。

public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
public class ProviderOptions
{
    public string Authority { get; set; }
    public string MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
}

DI システム内にプロバイダー オプションを登録し、適切な値を構成します。

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.Authority = "...";
        options.MetadataUrl = "...";
        options.ClientId = "...";
        options.DefaultScopes = new List<string> { "openid", "profile", "myApi" };
        options.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ResponseType = "...";
        options.ResponseMode = "...";
    });

前の例では、通常の文字列リテラルを使ってリダイレクト URI を設定しています。 次の代替手段も使用できます。

  • IWebAssemblyHostEnvironment.BaseAddress を使用した TryCreate:

    Uri.TryCreate(
        $"{builder.HostEnvironment.BaseAddress}authentication/login-callback", 
        UriKind.Absolute, out var redirectUri);
    options.RedirectUri = redirectUri;
    
  • ホスト ビルダーの構成:

    options.RedirectUri = builder.Configuration["RedirectUri"];
    

    wwwroot/appsettings.json:

    {
      "RedirectUri": "https://localhost:5001/authentication/login-callback"
    }
    

その他のリソース