ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Blazor WebAssembly アプリケーションを Azure App Service にデプロイして Azure AD で保護する

f:id:masatsuna:20220104234505p:plain

新年 1 つ目のポストは、昨年末すごくはまった内容をまとめてみようと思います。

やりたいこと

.NET ベースの Blazor WebAssembly アプリケーションを ASP.NET Core のアプリケーションでホストして、 Azure App Service に発行します。 こいつに対して Azure AD ( B2C ではない方) で認証をかけていきます。

Blazor WebAssembly のアプリケーションは、 Azure Static Web Apps を使うケースが多いかもしれません。 しかし、規模の大きなシステムだと「複雑な業務要件」という名の低性能機能が紛れ込んだりして、 Azure Functions は安心して使えなかったりします。 そうなると結局 App Service Plan 上で動かすなんて言うことになったりするんですよね。 規模が大きくなればなるほど、こういった部分はコントロールできなくなっていくのがつらいところです。

環境

Blazor アプリケーションを作る

ASP.NET Core のアプリケーションをバックエンドにもつ Blazor WebAssembly のアプリケーションは、 Visual Studio のテンプレートを使うと簡単にひな形を作ることができます。 まずは [Blazor WebAssembly アプリ] を選択しましょう。

f:id:masatsuna:20220102121838p:plain

今回は Azure AD 認証を組み込むので、 [認証の種類] を [Microsoft ID プラットフォーム] に設定します。 またバックエンドの Web API は Azure App Service にホストするので、 [ASP.NET Core でホストとされた] にもチェックを入れてプロジェクトを生成します。

f:id:masatsuna:20220102121928p:plain

生成が終わると、以下のような 3 つのプロジェクトが作成されるはずです。

f:id:masatsuna:20220102122050p:plain

プロジェクト 概要
[*.Client] プロジェクト Blazor WebAssembly のソースコードが含まれています。クライアント側の処理本体です。
[*.Server] プロジェクト バックエンドの Web API を含む、 ASP.NET Core のアプリケーションです。ローカルで実行する場合はこのプロジェクトを実行します。
[*.Shared] プロジェクト 主にバックエンドの公開する Web APIDTO*1 を格納するプロジェクトです。サーバーサイドとクライアントサイドで共有したい簡単な処理もここに置きます。

Azure AD 認証の設定を確認する

ここまでの設定で生成したアプリケーションには、 Azure AD 認証に必要なコードや設定が、ダミー値で含まれています。 重要な設定項目について簡単に触れておきます。

サーバーアプリケーションの appsettings.json

Web API を保護するための設定が appsettings.json にあります。 後ほど Azure Portal で設定した値をここに設定していくことになります。

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "qualified.domain.name",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111",
    "Scopes": "access_as_user",
    "CallbackPath": "/signin-oidc"
  },
  // 後略
}

クライアントアプリケーションの appsettings.json

こちらはクライアント側アプリケーションを保護するための設定です。 [*.Client] プロジェクトのルートの wwwroot ディレクトリ内にファイルがあります。 こちらも後ほど Azure Portal で設定した値を設定します。

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/22222222-2222-2222-2222-222222222222",
    "ClientId": "33333333-3333-3333-33333333333333333",
    "ValidateAuthority": true
  }
}

クライアントアプリケーションの Program.cs

[*.Client] プロジェクトの Program.cs にも設定があります。 今回の構成では、 AddMsalAuthentication メソッド内のオプション設定を変更することになります。

using BlazorApp.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddHttpClient("BlazorApp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("BlazorApp.ServerAPI"));

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("api://api.id.uri/access_as_user");
});

await builder.Build().RunAsync();

なお今回は動作検証が目的ですので、上記の設定ファイルやコードに設定値をべた書きします。 ローカル開発のことや、各種設定値を直接設定ファイルに保存するセキュリティ上の問題は全く考慮していませんので注意してください。

Azure に実行環境を構築する

アプリケーションの生成が完了したら、 Azure 上に実行環境を作っていきます。 今回は Windows の Azure App Service 上にアプリケーションを配置するので、以下のような設定で作ってみます。

f:id:masatsuna:20220102213549p:plain

リソースができたら、 URL を控えておきましょう。

Azure AD を設定する

次に Azure AD の設定を行っていきます。 クライアントアプリケーションとサーバーアプリケーションを Azure AD にアプリ登録して、クライアントアプリケーションからサーバーアプリケーションの呼び出しを許可するように設定しましょう。

サーバー API のアプリケーションを登録する

Azure Portal で [Azure Actice Directory] のブレードを開き、 [アプリ登録] のメニューから [新規登録] を行いましょう。 アプリケーションの [名前] を適当に設定して、 [サポートされているアカウントの種類] を [この組織ディレクトリのみに含まれるアカウント] に設定します。 [リダイレクト URI] は [Web] を選択して、リダイレクト先の URL は未設定にします。

f:id:masatsuna:20220102215413p:plain

設定が終わったら、画面下部の [登録] ボタンを押下します。

正常に登録ができると、登録したアプリの詳細が表示されるので、以下の設定値を手元に控えましょう。 どちらも GUID 値です。

  • アプリケーション (クライアント) ID
  • ディレクトリ (テナント) ID

続いて [API の公開] メニューに移動し、 [Scope の追加] を押下します。 [アプリケーション ID の URI] が表示されるので、控えておきましょう。 画面下部の [保存してから続ける] を押下します。

f:id:masatsuna:20220102230942p:plain

[スコープ名] 、 [管理者の同意の表示名] 、 [管理者の同意の説明] を設定します。 [スコープ名] は後で利用するため控えておきましょう。 [状態] を [有効] にして、画面下部の [スコープの追加] を押下します。

f:id:masatsuna:20220103000407p:plain

クライアントアプリケーションを登録する

続いてクライアントアプリケーションも Azure AD に登録します。 [アプリ登録] のメニューから [新規登録] を行います。 アプリケーションの [名前] を適当に設定して、 [サポートされているアカウントの種類] を [この組織ディレクトリのみに含まれるアカウント] に設定します。 [リダイレクト URI] は [シングルページ アプリケーション (SPA)] を選択して、リダイレクト先の URL は [<Azure App Service の URL>/authentication/login-callback] に設定します。

f:id:masatsuna:20220103005205p:plain

設定が終わったら、画面下部の [登録] ボタンを押下します。

正常に登録ができると、登録したアプリの詳細が表示されるので、以下の設定値を手元に控えましょう。

  • アプリケーション (クライアント) ID

続いて [API のアクセス許可] メニューに移動し、 [アクセス許可の追加] を押下します。 [自分の API] を選択して、先ほど作成したサーバー API のアプリケーションを選択します。 作成したスコープを選択して、 [アクセス許可の追加] を押下します。

f:id:masatsuna:20220103010015p:plain

[<テナント名> に管理者の同意を与えます] ボタンを押下して、同意を付与します。

f:id:masatsuna:20220103010936p:plain

Azure AD の情報取得

プライマリドメインの値を控えておきましょう。 [Azure Active Directory] ブレードの [概要] メニューの画面で確認できます。 通常は 「xxxxxx.onmicrosoft.com」となります。

設定値の修正とコードの修正

Azure Portal から取得した各情報を、テンプレートのJSONやコードに反映していきます。

サーバーアプリケーションの appsettings.json の設定

DomainTenantIdClientIdScopes の値を設定します。

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<プライマリドメインの値>",
    "TenantId": "<ディレクトリ (テナント) ID の GUID 値>",
    "ClientId": "<サーバー API のアプリケーションのアプリケーション (クライアント) ID>",
    "Scopes": "<サーバー API のアプリケーションのスコープ名>",
    "CallbackPath": "/signin-oidc"
  },
  // 後略
}

クライアントアプリケーションの appsettings.json の設定

AuthorityClientId の値を設定します。

  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/<ディレクトリ (テナント) ID の GUID 値>",
    "ClientId": "<クライアントアプリケーションのアプリケーション (クライアント) ID>",
    "ValidateAuthority": true
  }

クライアントアプリケーションの Program.cs の設定

DefaultAccessTokenScopes に以下のように設定します。

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("<サーバー API のアプリケーションのアプリケーション ID の URI>/<スコープ名>");
});

なおここまでの手順は、以下のリンク先にも解説があります。

docs.microsoft.com

アプリケーションの配置

アプリケーションの修正が終わったら、 [*.Server] のプロジェクトを発行して、 Azure App Service に配置しましょう。 Release モードで発行するようにしましょう。

f:id:masatsuna:20220104221302p:plain

実行。。。だがしかし

Microsoft の手順では、これで Azure AD 認証が実行できるはずなのですが、動かしてみると多分動かないと思います。 Azure App Service に配置したアプリケーションにブラウザーからアクセスすると、 Blazor のアプリケーションが動いていることは確認できます。 しかし、右上の [Log in] リンクを押下すると、以下のような画面になってしまいます。

f:id:masatsuna:20220104221524p:plain

画面のメッセージは以下のように表示されています。

There was an error trying to log you in: 'Cannot read properties of undefined (reading 'toLowerCase')'

このメッセージは、ほとんどのケースで JavaScript の読み込みがうまくできていないときに表示されるものです。 何かしらの処理を呼び出そうとした結果、その処理が存在しないと、このようなメッセージが表示されます。

ブラウザーの開発者ツールを見てみると、コンソールにこんなメッセージが表示されています。

f:id:masatsuna:20220104222458p:plain

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

以下のトラブルシューティングにも同じようなメッセージについて記載がありました。

https://docs.microsoft.com/ja-jp/aspnet/core/blazor/security/webassembly/hosted-with-azure-active-directory?view=aspnetcore-6.0#troubleshoot

f:id:masatsuna:20220104222739p:plain

しかし、 Azure Portal から上述の設定箇所を見ても、既定値では問題はない状態になっていると思います。

何が原因なのか

この問題は、 Azure AD 認証に使用する MSAL のクライアントライブラリから連なるライブラリが、トリミングの中で削除されてしまうことにあります。 Blazor WebAssembly は、クライアントに出力するライブラリサイズを削減するために、発行ビルドを行ったタイミングでトリミングが行われます。 本来必要なライブラリがトリミングによって削除されてしまったことで、このような現象が発生しているのです。

docs.microsoft.com

この挙動については、以下の GitHub Issue でも解説があります。

github.com

解決策1:トリミングを無効にする

まずは最も手軽に実施できる、トリミングを無効にする方法で解決してみましょう。 [*.Client] プロジェクトの csproj ファイルを開き、以下のように PublishTrimmed 要素を追加して false に設定します。

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <PublishTrimmed>false</PublishTrimmed>
  </PropertyGroup>

この設定で再度発行を行います。 ブラウザーからアクセスすると、 Azure AD 認証が実行できます。 [Log in] のリンクを押下すると、以下のように Azure AD 認証の画面がポップアップします。

f:id:masatsuna:20220104230355p:plain

正常にログインもできるはずです。

解決策2:MSAL の必要ライブラリが削除されないようにする

続いて、 MSAL の必要ライブラリがトリミングされないように設定する方法で解決してみます。 解決策1 で設定した PublishTrimmed 要素の設定を削除して、以下のように <TrimmerRootAssembly Include="Microsoft.Authentication.WebAssembly.Msal" /> の設定を追加します。

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="6.0.1" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
    <TrimmerRootAssembly Include="Microsoft.Authentication.WebAssembly.Msal" />
  </ItemGroup>

これで再度発行を行っても、 Azure AD 認証が実行できます。

うまくいかないとき

これらの解決策を実行して、画面がうまく表示できないときは、 Visual Studiodotnet のローカルキャッシュが残ってしまっている影響が考えられます。 ソリューションフォルダ内の bin や obj ディレクトリをいったん削除して、再度発行を行ってみましょう。

どちらがより良い解決策か

PublishTrimmedfalse に設定すると、すべてのトリミングが無効になります。 そのため、解決策2 で示した方法のほうが、良い解決策といえます。

実際にこれら 2 つの解決策を実施して、ブラウザーがダウンロードする HTML やライブラリの合計サイズを比較してみると、以下のようになりました。

手段 サイズ
解決策1(トリミング無効設定) 728KB
解決策2(TrimmerRootAssembly 設定) 676KB

これは、トップ画面をロードするときのダウンロードサイズで比較しています。 純粋なライブラリの量だけではなく、 HTML や CSS などを含むダウンロードサイズであることに注意して下さい。

そのような雑な条件下でも、依存関係にあるライブラリを個別にトリミングしないよう設定した方が、ダウンロードサイズを小さくできました。 パフォーマンスを求めるのであれば、細かな制御が必須であることは明らかです。

まとめ

今回は Blazor WebAssembly のアプリケーションに対して Azure AD 認証をかける方法について解説しました。 Blazor WebAssembly 特有のトリミングにより、ドキュメントに記載されていない挙動に悩まされましたが、問題なく認証をかけることができました。

ローカル開発環境ではうまく動くのに、発行したとたん動かないようなケースは、トリミングを疑ってみるとよいかもしれません。 トリミングの影響かどうか調査する目的なら、 PublishTrimmed の設定を false にするのがお手軽な方法です。 トリミングの影響であることが確定したら、細かなオプションを活用して、トリミングによるパフォーマンスアップと、機能性の担保を両立させる設定を探すのが定石だと思います。

なおトリミングには、今回ご紹介した以外に様々な設定があります。 詳細は以下のドキュメントを参照してください。

docs.microsoft.com

*1:Data Transfer Object