ASP.NET Core Blazor WebAssembly で Graph API を使用する

注意

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

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

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

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

この記事では、Blazor WebAssembly アプリでの Microsoft Graph を使用して、アプリが Microsoft Cloud リソースにアクセスできるようにする方法について説明します。

次の 2 つの方法について説明します。

  • Graph SDK: Microsoft Graph SDK は、Microsoft Graph にアクセスする高品質かつ効率的で、回復性を備えたアプリの構築を簡素化します。 この方法を使うには、この記事の上部にある [Graph SDK] ボタンを選んでください。

  • 名前付き HttpClient と Graph API: 名前付き HttpClient は、Microsoft Graph API 要求を Microsoft Graph に直接発行できます。 この方法を使うには、この記事の上部にある [名前付き HttpClient と Graph API] ボタンを選んでください。

この記事のガイダンスは、Microsoft Graph のドキュメントと、他の Microsoft ドキュメント セットでの Azure セキュリティ ガイダンスを置き換えることを目的としたものではありません。 運用環境で Microsoft Graph を実装する前に、この記事の「その他のリソース」セクションのセキュリティ ガイダンスを評価してください。 Microsoft のベスト プラクティスに従って、アプリの脆弱性を制限してください。

Microsoft Graph と Blazor WebAssembly を操作するための追加の方法は、次の Microsoft Graph と Azure のサンプルで提供されています。

上記の 2 つのサンプルのいずれかに関するフィードバックを提供するには、サンプルの GitHub リポジトリで問題を開きます。 Azure サンプルの問題を開いている場合は、Azure サンプル リポジトリ (Azure-Samples) に多数のサンプルが含まれているため、開始コメントにサンプルへのリンクを指定します。 問題を詳細に説明し、必要に応じてサンプル コードを含めます。 問題またはエラーを再現する最小限のアプリを GitHub に配置します。 パブリック リポジトリにコミットする前に、サンプルから Azure アカウント構成データを削除してください。

この記事または ASP.NET Core に関するフィードバックを提供したり、支援を求めたりするには、ASP.NET Core Blazor の基礎をご覧ください。

重要

この記事で説明するシナリオは、AAD B2C ではなく、Microsoft Entra (ME-ID) を identity プロバイダーとして使う場合に適用されます。 クライアント側の Blazor WebAssembly アプリと AAD B2C identity プロバイダーでの Microsoft Graph の使用は、現在、サポートされていません。これは、クライアント側の Blazor アプリではセキュリティで保護できないクライアント シークレットがアプリに必要になるためです。 AAD B2C スタンドアロン Blazor WebAssembly アプリで Graph API を使用する場合は、ユーザーに代わって Graph API にアクセスするバックエンド サーバー (Web) API を作成します。 クライアント側のアプリは、Web API を呼び出すユーザーを認証および認可します。これは、安全に Microsoft Graph にアクセスし、サーバーベースの Web API からクライアント側の Blazor アプリにデータを返すためです。 クライアント シークレットは、クライアント上の Blazor アプリではなく、サーバーベースの Web API で安全に管理されます。 クライアント側の Blazor アプリにクライアント シークレットを保存しないでください。

ホストされた Blazor WebAssembly アプリの使用がサポートされています。その場合、Server アプリは Graph SDK または API を使って、Web API 経由で Graph データを Client アプリに提供します。 詳しくは、この記事の「ホストされた Blazor WebAssembly ソリューション」セクションをご覧ください。

この記事の例では、新しい .NET/C# の機能を利用します。 この例に .NET 7 以前を使用する場合は、若干の変更が必要です。 ただし、Microsoft Graph の操作に関連するテキストとコードの例は、ASP.NET Core のすべてのバージョンで同じです。

"次のガイダンスは、Microsoft Graph v5 に適用されます。"

Blazor アプリで使うための Microsoft Graph SDK は、"Microsoft Graph .NET クライアント ライブラリ" と呼ばれます。

Graph SDK の例の場合、スタンドアロン Blazor WebAssembly アプリでは、次のパッケージ参照が必要です。 アプリで MSAL 認証が有効になっている場合は、最初の 2 つのパッケージが既に参照されています。たとえば、「Microsoft Entra ID を使用して ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」のガイダンスに従って作成したアプリなどです。

Graph SDK の例の場合、スタンドアロン Blazor WebAssembly アプリまたはホストされた Blazor WebAssembly ソリューションの Client アプリでは、次のパッケージ参照が必要です。 アプリで MSAL 認証が有効になっている場合は、最初の 2 つのパッケージが既に参照されています。たとえば、「Microsoft Entra ID を使用して ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」のガイダンスに従って作成したアプリなどです。

Note

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

Azure portal で、アプリがユーザーの代わりにアクセスできる Microsoft Graph データの委任されたアクセス許可 (スコープ)†を付与します。 この記事の例では、アプリの登録には、ユーザー データを読み取るための委任されたアクセス許可 (API アクセス許可Microsoft.Graph>User.Read スコープ、型: 委任) が含まれている必要があります。 User.Read スコープを使用すると、ユーザーはアプリにサインインでき、アプリはサインインしているユーザーのプロフィールと会社情報を読み取ることができます。 詳細については、「Microsoft identity プラットフォームでのアクセス許可と同意の概要」および「Microsoft Graph のアクセス許可の概要」をご覧ください。

†"アクセス許可" と "スコープ" は、同じことを意味し、セキュリティ ドキュメントと Azure portal で意味の区別なく使用されます。 この記事では、本文が Azure portal を参照していない限り、Graph のアクセス許可を参照するときにスコープ/を使用します。

スコープでは大文字と小文字が区別されないため、User.Readuser.read と同じです。 どちらの形式でも自由に使用できますが、アプリケーション コード間で一貫した選択肢をお勧めします。

Azure portal でアプリの登録に Microsoft Graph API スコープを追加した後、次のアプリ設定構成をアプリの wwwroot/appsettings.json ファイルに追加します。これには、Microsoft Graph のバージョンとスコープを含む Graph ベースの URL が含まれます。 次の例の User.Read スコープは、この記事の後のセクションにある例のために指定されています。 スコープでは大文字と小文字が区別されません。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

前の例で、{VERSION} プレースホルダーは Microsoft Graph API のバージョンです (例: v1.0)。

identity プロバイダーとして ME-ID を使用するアプリの完全な wwwroot/appsettings.json 構成ファイルの例を次に示します。ここでは、Microsoft Graph に対してユーザー データ (user.read スコープ) の読み取りが指定されています。

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

前の例では、{TENANT ID} プレースホルダーはディレクトリ (テナント) ID、 {CLIENT ID} プレースホルダーはアプリケーション (クライアント) ID です。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

スタンドアロン アプリに次の GraphClientExtensions クラスを追加します。 スコープは、AuthenticateRequestAsync メソッドの AccessTokenRequestOptionsScopes プロパティに提供されます。

ホストされている Blazor WebAssembly ソリューションのスタンドアロン アプリまたは Client アプリに次の GraphClientExtensions クラスを追加します。 スコープは、AuthenticateRequestAsync メソッドの AccessTokenRequestOptionsScopes プロパティに提供されます。

アクセス トークンが取得されない場合、次のコードで Graph 要求のベアラー承認ヘッダーは設定されません。

GraphClientExtensions.cs=

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using IAccessTokenProvider = 
    Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
            this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                new HttpClient(),
                sp.GetRequiredService<IAuthenticationProvider>(),
                baseUrl);
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(RequestInformation request, 
            Dictionary<string, object>? additionalAuthenticationContext = null, 
            CancellationToken cancellationToken = default)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                {
                    Scopes = 
                        config.GetSection("MicrosoftGraph:Scopes").Get<string[]>() ??
                        [ "user.read" ]
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Add("Authorization", 
                    $"{CoreConstants.Headers.Bearer} {token.Value}");
            }
        }
    }
}

重要

前のコードが AdditionalScopesToConsent ではなくスコープを追加するために DefaultAccessTokenScopes を使用する理由については、「DefaultAccessTokenScopesAdditionalScopesToConsent」のセクションを参照してください。

Program ファイルで、AddGraphClient 拡張メソッドを使用して Graph クライアント サービスと構成を追加します。 次のコードでは、アプリ設定ファイルにこれらの設定が見つからない場合、バージョン 1.0 の Microsoft Graph ベース アドレスと User.Read スコープが既定値になります。

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Graph SDK を使用してコンポーネントから Graph API を呼び出す

次の UserData コンポーネントでは、挿入された GraphServiceClient を使用してユーザーの ME-ID プロファイル データを取得し、その携帯電話番号を表示します。

ME-ID でテスト ユーザーを作成する場合は、ユーザーの ME-ID プロファイルに、Azure portal の携帯電話番号を指定してください。

UserData.razor=

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.Models.User? user;

    protected override async Task OnInitializedAsync()
    {
        user = await Client.Me.GetAsync();
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

ヒント

アプリにユーザーを追加するには、「アプリロールの有無にかかわらず、アプリの登録にユーザーを割り当てる」セクションを参照してください。

Graph SDK をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しいプライベートまたはシークレット ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

Graph SDK を使用してユーザー クレームをカスタマイズする

次の例のアプリでは、ME-ID ユーザー プロファイルのデータから、ユーザーの携帯電話番号とオフィスの場所のクレームを作成します。 このアプリには、ME-ID で構成された User.Read Graph API スコープが必要です。 このシナリオのテスト ユーザーは、ME-ID プロファイルに携帯電話番号とオフィスの場所を持っている必要があります。これは、Azure portal を使って追加できます。

次のカスタム ユーザー アカウント ファクトリで:

  • ILogger (logger) は、CreateUserAsync メソッドで情報やエラーをログする必要がある場合に便利なように組み込まれています。
  • AccessTokenNotAvailableException がスローされた場合、ユーザーは自分のアカウントにサインインするために identity プロバイダーにリダイレクトされます。 アクセス トークンの要求が失敗した場合は、追加または異なるアクションを実行できます。 たとえば、アプリで AccessTokenNotAvailableException をログし、さらに調査するためにサポート チケットを作成できます。
  • フレームワークの RemoteUserAccount はユーザーのアカウントを表します。 RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで必要な場合は、そのカスタム ユーザー アカウント クラスを次のコード内の RemoteUserAccount と入れ替えます。

CustomAccountFactory.cs=

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

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger,
        IConfiguration config) 
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;
    private readonly string? baseUrl = string.Join("/",
        config.GetSection("MicrosoftGraph")["BaseUrl"] ?? 
            "https://graph.microsoft.com",
        config.GetSection("MicrosoftGraph")["Version"] ??
            "v1.0");

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

        if (initialUser.Identity is not null &&
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl))
            {
                try
                {
                    var client = new GraphServiceClient(
                        new HttpClient(),
                        serviceProvider
                            .GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);

                    var user = await client.Me.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

カスタム ユーザー アカウント ファクトリを使うように MSAL 認証を構成します。

Program ファイルで Microsoft.AspNetCore.Components.WebAssembly.Authentication 名前空間が使われていることを確認します。

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

このセクションの例は、wwwroot/appsettings.json ファイルの MicrosoftGraph セクションを使用してアプリ構成からバージョンが指定されたベース URL とスコープを読み取る方法に基づいています。 この記事の前半のガイダンスに従うと、Program ファイルには次の行が既に存在しているはずです。

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Program ファイルで、AddMsalAuthentication 拡張メソッドの呼び出しを見つけます。 コードを次のように更新します。これには、CustomAccountFactory を使ってアカウント クレーム プリンシパル ファクトリを追加する AddAccountClaimsPrincipalFactory への呼び出しが含まれます。

RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで使用されている場合は、そのカスタム ユーザー アカウント クラスを次のコード内の RemoteUserAccount と入れ替えます。

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();

次の UserClaims コンポーネントを使い、ユーザーが ME-ID で認証を行った後でユーザーのクレームを調査できます。

UserClaims.razor=

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

<h1>User Claims</h1>

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

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

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

Graph SDK をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しいプライベートまたはシークレット ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

次のガイダンスは、Microsoft Graph v4 に適用されます。 アプリを SDK v4 から v5 にアップグレードする場合は、「Microsoft Graph .NET SDK v5 の変更ログとアップグレード ガイド」を参照してください。

Blazor アプリで使うための Microsoft Graph SDK は、"Microsoft Graph .NET クライアント ライブラリ" と呼ばれます。

Graph SDK の例の場合、スタンドアロン Blazor WebAssembly アプリでは、次のパッケージ参照が必要です。 アプリで MSAL 認証が有効になっている場合は、最初の 2 つのパッケージが既に参照されています。たとえば、「Microsoft Entra ID を使用して ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」のガイダンスに従って作成したアプリなどです。

Graph SDK の例の場合、スタンドアロン Blazor WebAssembly アプリまたはホストされた Blazor WebAssembly ソリューションの Client アプリでは、次のパッケージ参照が必要です。 アプリで MSAL 認証が有効になっている場合は、最初の 2 つのパッケージが既に参照されています。たとえば、「Microsoft Entra ID を使用して ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」のガイダンスに従って作成したアプリなどです。

Note

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

Azure portal で、アプリがユーザーの代わりにアクセスできる Microsoft Graph データの委任されたアクセス許可 (スコープ)†を付与します。 この記事の例では、アプリの登録には、ユーザー データを読み取るための委任されたアクセス許可 (API アクセス許可Microsoft.Graph>User.Read スコープ、型: 委任) が含まれている必要があります。 User.Read スコープを使用すると、ユーザーはアプリにサインインでき、アプリはサインインしているユーザーのプロフィールと会社情報を読み取ることができます。 詳細については、「Microsoft identity プラットフォームでのアクセス許可と同意の概要」および「Microsoft Graph のアクセス許可の概要」をご覧ください。

†"アクセス許可" と "スコープ" は、同じことを意味し、セキュリティ ドキュメントと Azure portal で意味の区別なく使用されます。 この記事では、本文が Azure portal を参照していない限り、Graph のアクセス許可を参照するときにスコープ/を使用します。

スコープでは大文字と小文字が区別されないため、User.Readuser.read と同じです。 どちらの形式でも自由に使用できますが、アプリケーション コード間で一貫した選択肢をお勧めします。

Azure portal でアプリの登録に Microsoft Graph API スコープを追加した後、次のアプリ設定構成をアプリの wwwroot/appsettings.json ファイルに追加します。これには、Microsoft Graph のバージョンとスコープを含む Graph ベースの URL が含まれます。 次の例の User.Read スコープは、この記事の後のセクションにある例のために指定されています。 スコープでは大文字と小文字が区別されません。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

前の例で、{VERSION} プレースホルダーは Microsoft Graph API のバージョンです (例: v1.0)。

identity プロバイダーとして ME-ID を使用するアプリの完全な wwwroot/appsettings.json 構成ファイルの例を次に示します。ここでは、Microsoft Graph に対してユーザー データ (user.read スコープ) の読み取りが指定されています。

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

前の例では、{TENANT ID} プレースホルダーはディレクトリ (テナント) ID、 {CLIENT ID} プレースホルダーはアプリケーション (クライアント) ID です。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

スタンドアロン アプリに次の GraphClientExtensions クラスを追加します。 スコープは、AuthenticateRequestAsync メソッドの AccessTokenRequestOptionsScopes プロパティに提供されます。 HttpClient が Microsoft Graph から応答を受け取るのにより長い時間をかけられるよう、IHttpProvider.OverallTimeout は既定値の 100 秒から 300 秒に増やされています。

ホストされている Blazor WebAssembly ソリューションのスタンドアロン アプリまたは Client アプリに次の GraphClientExtensions クラスを追加します。 スコープは、AuthenticateRequestAsync メソッドの AccessTokenRequestOptionsScopes プロパティに提供されます。 HttpClient が Microsoft Graph から応答を受け取るのにより長い時間をかけられるよう、IHttpProvider.OverallTimeout は既定値の 100 秒から 300 秒に増やされています。

アクセス トークンが取得されない場合、次のコードで Graph 要求のベアラー承認ヘッダーは設定されません。

GraphClientExtensions.cs=

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
        this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp =>
            new HttpClientHttpProvider(new HttpClient()));

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                baseUrl,
                sp.GetRequiredService<IAuthenticationProvider>(),
                sp.GetRequiredService<IHttpProvider>());
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                { 
                    Scopes = config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Authorization ??= new AuthenticationHeaderValue(
                    "Bearer", token.Value);
            }
        }
    }

    private class HttpClientHttpProvider(HttpClient client) : IHttpProvider
    {
        private readonly HttpClient client = client;

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return client.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return client.SendAsync(request, completionOption, cancellationToken);
        }

        public void Dispose()
        {
        }
    }
}

重要

前のコードが AdditionalScopesToConsent ではなくスコープを追加するために DefaultAccessTokenScopes を使用する理由については、「DefaultAccessTokenScopesAdditionalScopesToConsent」のセクションを参照してください。

Program ファイルで、AddGraphClient 拡張メソッドを使用して Graph クライアント サービスと構成を追加します。

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Graph SDK を使用してコンポーネントから Graph API を呼び出す

次の UserData コンポーネントでは、挿入された GraphServiceClient を使用してユーザーの ME-ID プロファイル データを取得し、その携帯電話番号を表示します。 ME-ID でテスト ユーザーを作成する場合は、ユーザーの ME-ID プロファイルに、Azure portal の携帯電話番号を指定してください。

UserData.razor=

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.User? user;

    protected override async Task OnInitializedAsync()
    {
        var request = Client.Me.Request();
        user = await request.GetAsync();
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

ヒント

アプリにユーザーを追加するには、「アプリロールの有無にかかわらず、アプリの登録にユーザーを割り当てる」セクションを参照してください。

Graph SDK をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しいプライベートまたはシークレット ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

Graph SDK を使用してユーザー クレームをカスタマイズする

次の例のアプリでは、ME-ID ユーザー プロファイルのデータから、ユーザーの携帯電話番号とオフィスの場所のクレームを作成します。 このアプリには、ME-ID で構成された User.Read Graph API スコープが必要です。 このシナリオのテスト ユーザーは、ME-ID プロファイルに携帯電話番号とオフィスの場所を持っている必要があります。これは、Azure portal を使って追加できます。

次のカスタム ユーザー アカウント ファクトリで:

  • ILogger (logger) は、CreateUserAsync メソッドで情報やエラーをログする必要がある場合に便利なように組み込まれています。
  • AccessTokenNotAvailableException がスローされた場合、ユーザーは自分のアカウントにサインインするために identity プロバイダーにリダイレクトされます。 アクセス トークンの要求が失敗した場合は、追加または異なるアクションを実行できます。 たとえば、アプリで AccessTokenNotAvailableException をログし、さらに調査するためにサポート チケットを作成できます。
  • フレームワークの RemoteUserAccount はユーザーのアカウントを表します。 RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで必要な場合は、そのカスタム ユーザー アカウント クラスを次のコード内の RemoteUserAccount と入れ替えます。

CustomAccountFactory.cs=

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

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor, 
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;

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

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = ActivatorUtilities
                        .CreateInstance<GraphServiceClient>(serviceProvider);
                    var request = client.Me.Request();
                    var user = await request.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

カスタム ユーザー アカウント ファクトリを使うように MSAL 認証を構成します。

Program ファイルで Microsoft.AspNetCore.Components.WebAssembly.Authentication 名前空間が使われていることを確認します。

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

このセクションの例は、wwwroot/appsettings.json ファイルの MicrosoftGraph セクションを使用してアプリ構成からバージョンが指定されたベース URL とスコープを読み取る方法に基づいています。 この記事の前半のガイダンスに従うと、Program ファイルには次の行が既に存在しているはずです。

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Program ファイルで、AddMsalAuthentication 拡張メソッドの呼び出しを見つけます。 コードを次のように更新します。これには、CustomAccountFactory を使ってアカウント クレーム プリンシパル ファクトリを追加する AddAccountClaimsPrincipalFactory への呼び出しが含まれます。

RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで使用されている場合は、そのカスタム ユーザー アカウント クラスを次のコード内の RemoteUserAccount と入れ替えます。

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();

次の UserClaims コンポーネントを使い、ユーザーが ME-ID で認証を行った後でユーザーのクレームを調査できます。

UserClaims.razor=

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

<h1>User Claims</h1>

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

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

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

Graph SDK をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しいプライベートまたはシークレット ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

次の例では、Graph API の呼び出しに名前付き HttpClient を使ってユーザーの携帯電話番号を取得し、通話を処理するか、携帯電話番号のクレームとオフィスの場所のクレームを含むようにユーザーのクレームをカスタマイズします。

例には、スタンドアロン Blazor WebAssembly アプリの Microsoft.Extensions.Http に対するパッケージ参照が必要です。

この例での、スタンドアロン Blazor WebAssembly アプリまたはホストされた Blazor WebAssembly ソリューションの Client アプリには、Microsoft.Extensions.Http に対する参照が必要です。

Note

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

Azure portal で、アプリがユーザーの代わりにアクセスできる Microsoft Graph データの委任されたアクセス許可 (スコープ)†を付与します。 この記事の例では、アプリの登録には、ユーザー データを読み取るための委任されたアクセス許可 (API アクセス許可Microsoft.Graph>User.Read スコープ、型: 委任) が含まれている必要があります。 User.Read スコープを使用すると、ユーザーはアプリにサインインでき、アプリはサインインしているユーザーのプロフィールと会社情報を読み取ることができます。 詳細については、「Microsoft identity プラットフォームでのアクセス許可と同意の概要」および「Microsoft Graph のアクセス許可の概要」をご覧ください。

†"アクセス許可" と "スコープ" は、同じことを意味し、セキュリティ ドキュメントと Azure portal で意味の区別なく使用されます。 この記事では、本文が Azure portal を参照していない限り、Graph のアクセス許可を参照するときにスコープ/を使用します。

スコープでは大文字と小文字が区別されないため、User.Readuser.read と同じです。 どちらの形式でも自由に使用できますが、アプリケーション コード間で一貫した選択肢をお勧めします。

Azure portal でアプリの登録に Microsoft Graph API スコープを追加した後、次のアプリ設定構成をアプリの wwwroot/appsettings.json ファイルに追加します。これには、Microsoft Graph のバージョンとスコープを含む Graph ベースの URL が含まれます。 次の例の User.Read スコープは、この記事の後のセクションにある例のために指定されています。 スコープでは大文字と小文字が区別されません。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

前の例で、{VERSION} プレースホルダーは Microsoft Graph API のバージョンです (例: v1.0)。

identity プロバイダーとして ME-ID を使用するアプリの完全な wwwroot/appsettings.json 構成ファイルの例を次に示します。ここでは、Microsoft Graph に対してユーザー データ (user.read スコープ) の読み取りが指定されています。

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

前の例では、{TENANT ID} プレースホルダーはディレクトリ (テナント) ID、 {CLIENT ID} プレースホルダーはアプリケーション (クライアント) ID です。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

Graph API を操作するために、Program ファイルで次の GraphAuthorizationMessageHandler クラスとプロジェクト構成を作成します。 ベース URL とスコープは、構成からハンドラーに提供されます。

GraphAuthorizationMessageHandler.cs=

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

namespace BlazorSample;

public class GraphAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigation, IConfiguration config)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: [ 
                string.Join("/",
                    config.GetSection("MicrosoftGraph")["BaseUrl"] ??
                        "https://graph.microsoft.com",
                    config.GetSection("MicrosoftGraph")["Version"] ??
                        "v1.0")
            ],
            scopes: config.GetSection("MicrosoftGraph:Scopes")
                        .Get<List<string>>() ?? [ "user.read" ]);
    }
}

承認された URL の末尾のスラッシュ (/) が必要です。 上記のコードは、アプリ設定の構成から次の承認済み URL を構築するか、アプリ設定の構成がない場合は、既定で次の承認済み URL を使用します: https://graph.microsoft.com/v1.0/

Program ファイルで、Graph API の名前付き HttpClient を構成します。

builder.Services.AddTransient<GraphAuthorizationMessageHandler>();

builder.Services.AddHttpClient("GraphAPI",
        client => client.BaseAddress = new Uri(
            string.Join("/",
                builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
                    "https://graph.microsoft.com",
                builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
                    "v1.0",
                string.Empty)))
    .AddHttpMessageHandler<GraphAuthorizationMessageHandler>();

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

ベース アドレスの末尾のスラッシュ (/) が必要です。 上記のコードでは、末尾のスラッシュが存在することを確認するために、string.Join の 3 番目の引数は string.Empty になっています: https://graph.microsoft.com/v1.0/

名前付き HttpClient を使用してコンポーネントから Graph API を呼び出す

UserInfo.cs クラスは、JsonPropertyNameAttribute 属性と、ME-ID によって使われる JSON 名で、必要なユーザー プロファイル プロパティを指定します。 次の例では、ユーザーの携帯電話番号とオフィスの場所のプロパティを設定します。

UserInfo.cs=

using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}

次の UserData コンポーネントの HttpClient は、Graph API がユーザーのプロファイル データに対する要求を発行するために作成されます。 me リソース (me) が、バージョンが指定された Graph API 要求のベース URL に追加されます。 Graph によって返される JSON データは、UserInfo クラス プロパティに逆シリアル化されます。 次の例では、携帯電話番号が取得されます。 必要に応じて、同様のコードを追加して、ユーザーの ME-ID プロファイルのオフィスの場所を含めることができます (userInfo.OfficeLocation)。 アクセス トークン要求が失敗した場合、ユーザーは新しいアクセス トークンのためにアプリへのサインインにリダイレクトされます。

UserData.razor=

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [Authorize]
@inject IConfiguration Config
@inject IHttpClientFactory ClientFactory

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(userInfo?.MobilePhone))
{
    <p>Mobile Phone: @userInfo.MobilePhone</p>
}

@code {
    private UserInfo? userInfo;

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

            userInfo = await client.GetFromJsonAsync<UserInfo>("me");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

ヒント

アプリにユーザーを追加するには、「アプリロールの有無にかかわらず、アプリの登録にユーザーを割り当てる」セクションを参照してください。

次のシーケンスでは、Graph API スコープの新しいユーザー フローについて説明します。

  1. 新しいユーザーが初めてアプリにサインインします。
  2. ユーザーは、Azure の同意 UI でアプリを使用することに同意します。
  3. ユーザーは、Graph API データを初めて要求するコンポーネント ページにアクセスします。
  4. ユーザーは、Graph API スコープに同意するために Azure の同意 UI にリダイレクトされます。
  5. Graph API ユーザー データが返されます。

初期サインインでスコープのプロビジョニング (Graph API スコープに対する同意) を行う場合は、 Program ファイルの既定のアクセス トークン スコープとして MSAL 認証のスコープを指定します。

+ var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
+     .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

+   foreach (var scope in scopes)
+   {
+       options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
+   }
});

重要

前のコードが AdditionalScopesToConsent ではなくスコープを追加するために DefaultAccessTokenScopes を使用する理由については、「DefaultAccessTokenScopesAdditionalScopesToConsent」のセクションを参照してください。

アプリに上記の変更が加えられる場合、ユーザー フローは次のシーケンスを採用します。

  1. 新しいユーザーが初めてアプリにサインインします。
  2. ユーザーは、Azure の同意 UI でアプリと Graph API スコープの使用に同意します。
  3. ユーザーは、Graph API データを初めて要求するコンポーネント ページにアクセスします。
  4. Graph API ユーザー データが返されます。

Graph API をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しい InPrivate または Incognito ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

名前付き HttpClient を使用してユーザー クレームをカスタマイズする

次の例のアプリでは、ME-ID ユーザー プロファイルのデータから、ユーザーの携帯電話番号とオフィスの場所のクレームを作成します。 このアプリには、ME-ID で構成された User.Read Graph API スコープが必要です。 ME-ID のテスト ユーザー アカウントには、携帯電話番号とオフィスの場所のエントリが必要であり、これは Azure portal を使ってユーザー プロファイルに追加できます。

この記事で前に説明したガイダンスに従って UserInfo クラスをアプリにまだ追加していない場合は、次のクラスを追加し、JsonPropertyNameAttribute 属性と ME-ID で使われる JSON 名で、必要なユーザー プロファイル プロパティを指定します。 次の例では、ユーザーの携帯電話番号とオフィスの場所のプロパティを設定します。

UserInfo.cs=

using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}

次のカスタム ユーザー アカウント ファクトリで:

  • ILogger (logger) は、CreateUserAsync メソッドで情報やエラーをログする必要がある場合に便利なように組み込まれています。
  • AccessTokenNotAvailableException がスローされた場合、ユーザーは自分のアカウントにサインインするために identity プロバイダーにリダイレクトされます。 アクセス トークンの要求が失敗した場合は、追加または異なるアクションを実行できます。 たとえば、アプリで AccessTokenNotAvailableException をログし、さらに調査するためにサポート チケットを作成できます。
  • フレームワークの RemoteUserAccount はユーザーのアカウントを表します。 RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで必要な場合は、そのカスタム ユーザー アカウント クラスを次のコード内の RemoteUserAccount と入れ替えます。

CustomAccountFactory.cs=

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

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IHttpClientFactory clientFactory,
        ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IHttpClientFactory clientFactory = clientFactory;

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

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = clientFactory.CreateClient("GraphAPI");

                    var userInfo = await client.GetFromJsonAsync<UserInfo>("me");

                    if (userInfo is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            userInfo.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            userInfo.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

MSAL 認証は、カスタム ユーザー アカウント ファクトリを使うように構成されます。 最初に、Program ファイルで Microsoft.AspNetCore.Components.WebAssembly.Authentication 名前空間が使われていることを確認します。

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

Program ファイルで、AddMsalAuthentication 拡張メソッドの呼び出しを見つけます。 コードを次のように更新します。これには、CustomAccountFactory を使ってアカウント クレーム プリンシパル ファクトリを追加する AddAccountClaimsPrincipalFactory への呼び出しが含まれます。

RemoteUserAccount を拡張するカスタム ユーザー アカウント クラスがアプリで使われている場合は、アプリのカスタム ユーザー アカウント クラスを次のコードの RemoteUserAccount と入れ替えます。

builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, 
        CustomAccountFactory>();

前の例は、MSAL で ME-ID 認証が使用されるアプリの場合です。 OIDC と API 認証にも、同様のパターンがあります。 詳しくは、「ASP.NET Core Blazor WebAssembly のセキュリティに関するその他のシナリオ」の「ペイロード要求を使用してユーザーをカスタマイズする」セクションの例をご覧ください。

次の UserClaims コンポーネントを使い、ユーザーが ME-ID で認証を行った後でユーザーのクレームを調査できます。

UserClaims.razor=

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

<h1>User Claims</h1>

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

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

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu コンポーネント (Layout/NavMenu.razor) 内のコンポーネントのページへのリンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

Graph API をローカル環境でテストする場合は、残っている Cookie がテストに干渉しないよう、テストのたびに新しい InPrivate または Incognito ブラウザー セッションを使うことをお勧めします。 詳しくは、「Microsoft Entra ID を使用して、ASP.NET Core Blazor WebAssembly スタンドアロン アプリをセキュリティで保護する」をご覧ください。

アプリ ロールの有無にかかわらず、アプリの登録にユーザーを割り当てる

Azure portal で次の手順を使用して、アプリの登録にユーザーを追加し、ユーザーにロールを割り当てることができます。

ユーザーを追加するには、Azure portal の ME-ID 領域から [ユーザー] を選択します。

  1. [新しいユーザー]>[新しいユーザーの作成] を選択します。
  2. [ユーザーの作成] テンプレートを使用します。
  3. Identity 領域でユーザーの情報を入力します。
  4. 初期パスワードを生成したり、ユーザーが初めてサインインしたときに変更する初期パスワードを割り当てたりすることができます。 ポータルによって生成されたパスワードを使用する場合は、ここでメモしておきます。
  5. [作成] を選択してユーザーを作成してください。 [新しいユーザーの作成] インターフェイスを閉じたら、[更新] を選択してユーザー一覧を更新し、新しいユーザーを表示します。
  6. この記事の例では、ユーザーの一覧から名前を選択し、[プロパティ] を選択し、連絡先情報を編集し、携帯電話番号を指定して新しいユーザーに携帯電話番号を割り当てます。

"アプリ ロールなしで" アプリにユーザーを割り当てるには:

  1. Azure portal の ME-ID 領域で、[エンタープライズ アプリケーション] を開きます。
  2. 一覧からアプリを選択します。
  3. ユーザーおよびグループの選択
  4. [Add user/group](ユーザーまたはグループの追加) を選択します。
  5. ユーザーを選択します。
  6. 割り当てを選択します。

"アプリ ロールを使って" ユーザーをアプリに割り当てるには:

  1. Microsoft Entra ID グループとロールを使用した ASP.NET Core Blazor WebAssembly のガイダンスに従って、Azure portal でアプリの登録にロールを追加します。
  2. Azure portal の ME-ID 領域で、[エンタープライズ アプリケーション] を開きます。
  3. 一覧からアプリを選択します。
  4. ユーザーおよびグループの選択
  5. [Add user/group](ユーザーまたはグループの追加) を選択します。
  6. ユーザーを選択して、アプリにアクセスするためのロールを選択します。 ユーザーのすべてのロールが割り当てられるまで、アプリにユーザーを追加するプロセスを繰り返すと、複数のロールがユーザーに割り当てられます。 複数のロールを持つユーザーは、アプリのユーザーの [ユーザーとグループ] リストに、割り当てられたロールごとに 1 回一覧表示されます。
  7. 割り当てを選択します。

DefaultAccessTokenScopesAdditionalScopesToConsent

この記事の例では、AdditionalScopesToConsent ではなく DefaultAccessTokenScopes を使用して Graph API スコープをプロビジョニングします。

AdditionalScopesToConsent は、ユーザーが Azure の同意 UI を介して MSAL を使用して初めてアプリにサインインするときに Graph API スコープをプロビジョニングできないため、使用されません。 ユーザーが Graph SDK で初めて Graph API にアクセスしようとすると、例外が発生します。

Microsoft.Graph.Models.ODataErrors.ODataError: Access token is empty.

ユーザーが DefaultAccessTokenScopes 経由で提供された Graph API スコープをプロビジョニングした後、アプリは後続のユーザー サインインに AdditionalScopesToConsent を使用できます。 ただし、委任された Graph スコープを持つ新しいユーザーを定期的に追加したり、委任された Graph API スコープをアプリに追加したりする必要がある運用アプリでは、アプリ コードを変更しても意味がありません。

ユーザーがアプリに初めてサインインするときに Graph API アクセスのスコープをプロビジョニングする方法についての前述の説明は、次の場合にのみ適用されます。

  • Graph SDK を採用するアプリ。
  • Graph API の名前付き HttpClient アクセスを使用し、アプリへの最初のサインイン時に Graph スコープへの同意をユーザーに求めるアプリ。

ユーザーが最初のサインイン時に Graph スコープへの同意を求めない名前付き HttpClient を使用する場合、ユーザーは、事前構成済みの DelegatingHandler、名前付き HttpClient を介して "Graph API へのアクセスを最初に要求するとき" に、Graph API スコープの同意用の Azure の同意 UI にリダイレクトされます。 Graph スコープが名前付き HttpClient アプローチで最初に同意されていない場合、DefaultAccessTokenScopesAdditionalScopesToConsent もアプリによって呼び出されません。 詳細については、この記事の名前付き HttpClient の範囲をご覧ください。

ホストされた Blazor WebAssembly ソリューション

この記事の例は、スタンドアロン Blazor WebAssembly アプリから直接、またはホストされた Blazor WebAssemblyソリューションClient アプリから直接、Graph API で Graph SDK または名前付き HttpClient を使う方法に関係します。 この記事では、ホストされたソリューションの Client アプリで Web API を使ってソリューションの Server アプリを呼び出した後、Server アプリで Graph SDK または API を使って Microsoft Graph を呼び出し、Client アプリにデータを返すという追加のシナリオは説明されていません。 これはサポートされているアプローチですが、この記事では説明しません。 このアプローチを採用したい場合:

  • Client アプリから Server アプリに要求を発行して Client アプリにデータを返す Web API の側面については、ASP.NET Core Blazor アプリからの Web API の呼び出しに関するガイダンスに従ってください。
  • 一般的な ASP.NET Core アプリ (このシナリオでは、ソリューションの Server アプリ) での Graph SDK の使用については、主要な Microsoft Graph ドキュメントのガイダンスに従ってください。 Blazor WebAssembly プロジェクト テンプレートを使って、組織の承認 (単一組織/SingleOrg または複数組織/MultiOrg) と Microsoft Graph オプション (Visual Studio の [Microsoft identity プラットフォーム]>、[接続済みサービス]>、[Microsoft Graph のアクセス許可を追加する]、または .NET CLI の dotnet new コマンドの --calls-graph オプション) でホストされた Blazor WebAssembly ソリューション (ASP.NET Core でホストされた/-h|--hosted) を作成する場合、ソリューションの Server アプリは、ソリューションがプロジェクト テンプレートから作成されるときは Graph SDK を使うように構成されます。

その他のリソース

一般的なガイダンス

  • Microsoft Graph のドキュメント
  • Microsoft Graph サンプル Blazor WebAssembly アプリ: このサンプルでは、Microsoft Graph .NET SDK を使って Blazor WebAssembly アプリから Office 365 内のデータにアクセスする方法を示します。
  • Microsoft Graphを使用して .NET アプリをビルドする」チュートリアルと「Microsoft Graph サンプル ASP.NET Core アプリ」: これらのリソースは、Client アプリに代わって一般的な ASP.NET Core アプリとして Microsoft Graph にアクセスするように Server アプリが構成されている、"ホストされた" Blazor WebAssembly ソリューションに最も適しています。 Client アプリは、Web API を使って、Server アプリに Graph データの要求を行います。 これらのリソースは、クライアント側Blazor WebAssemblyアプリからの Graph の呼び出しには直接適用されませんが、リンクされたリソースの ME-ID アプリ構成と Microsoft Graph コーディング プラクティスは、スタンドアロン Blazor WebAssembly アプリに関連しており、一般的なベスト プラクティスについて参照する必要があります。

セキュリティ ガイダンス