/try-blazorwasm-standalone-singleOrg

🧪📝 Blazor wasm (AAD認証) をGitHub Pages, Cloudflare Pagesへデプロイしてみます。

Primary LanguageJavaScriptMIT LicenseMIT

try-blazorwasm-standalone-singleOrg

Status Site URL
Deploy to GitHub Pages https://maremare.github.io/try-blazorwasm-standalone-singleOrg
Azure Static Web Apps CI/CD https://polite-moss-0d2a72510.2.azurestaticapps.net
Cloudflare https://try-blazorwasm-standalone-singleorg.pages.dev

'Cannot read properties of undefined (reading 'toLowerCase')'

GitHub Pages あるいは Azure Static Web Apps へ発行し、実際にサイトへアクセスすると次のエラーが表示される。

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

対処方法は *.csproj に以下を追加すれば良いらしい。

<ItemGroup>
  <TrimmerRootAssembly Include="Microsoft.Authentication.WebAssembly.Msal" />
</ItemGroup>

詳細は以下:

'"undefined" is not valid JSON'

デプロイしたサイトへアクセスすると次のエラーが表示される。

There was an error trying to log you in: '"undefined" is not valid JSON'

対処方法は *.csproj に以下を追加すれば良いらしい。

<ItemGroup>
  <TrimmerRootAssembly Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
</ItemGroup>

詳細は以下:

リダイレクトの設定

詳細:

クライアントシークレットの設定

ユーザシークレットには以下のキーでクライアントシークレットを設定しているので、発行先の環境変数へ追加する必要がある。

{
  "AzureAD:ClientSecret": "<<Client Secret>>"
}

GitHub Pages

詳細:

Azure Static Web Apps

無料版ではダメだった:

Azure AD 認証と承認のエラー コード - Microsoft Entra | Microsoft Learn

  • AADSTS90043

    NationalCloudAuthCodeRedirection - 機能が無効になっています。

もしかして無料のホスティングプランだと設定できない?

以下が参考になりそう…

  1. Azure Static Web Apps の認証と承認 | Microsoft Learn (マネージド認証)

    1 構成済みの Azure Active Directory プロバイダーは、Microsoft アカウントでサインインを許可します。

    特定の Active Directory テナントにログインを制限するには、[カスタム Azure Active Directory プロバイダー] を構成します。

  2. Azure Static Web Apps でのカスタム認証 | Microsoft Learn (カスタム認証)

    カスタム認証は、Azure Static Web Apps Standard プランでのみ使用できます。

  3. Azure Static Web AppsのアプリにAzure ADカスタム認証機能を追加

    マネージド認証

    Azure AD のどのテナントでもログインできてしまいます。

    やりたいことは「カスタム認証」が適している。が、しかし無料が良かった… ここで断念。

Cloudflare Pages

Deploy a Blazor Site · Cloudflare Pages docs

詳細:

  • ビルドの構成
    • ビルドコマンド
      curl -sSL https://dot.net/v1/dotnet-install.sh > dotnet-install.sh;
      chmod +x dotnet-install.sh;
      ./dotnet-install.sh -c 8.0 -InstallDir ./dotnet8;
      ./dotnet8/dotnet --version;
      ./dotnet8/dotnet workload install wasm-tools;
      ./dotnet8/dotnet publish "src/blazorwasm-standalone-singleOrg" -c Release -o output;
      rm $(ls output/wwwroot/_framework/*.wasm);
    • ビルド出力ディレクトリ
      /output/wwwroot

Microsoft.Graph v4 to v5

Microsoft.Graph Version="4.54.0" → Microsoft.Graph Version="5.1.0" は破壊的変更がある模様。

以下が参考になりそう:

一時的な解決方法?:

参考:

解決方法:

GraphClientExtensions.cs:
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using IAccessTokenProvider = Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider;

/// <summary>
/// Adds services and implements methods to use Microsoft Graph SDK.
/// </summary>
internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes.IsNullOrEmpty())
        {
            return services;
        }

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

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

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

        return services;
    }

    /// <summary>
    /// Implements IAuthenticationProvider interface.
    /// Tries to get an access token for Microsoft Graph.
    /// </summary>
    private class GraphAuthenticationProvider : IAuthenticationProvider
    {
        private readonly IConfiguration _config;

        public GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, IConfiguration config)
        {
            this.TokenProvider = tokenProvider;
            this._config = config;
        }

        public IAccessTokenProvider TokenProvider { get; }

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

            if (result.TryGetToken(out var token))
            {
                request.Headers.Add("Authorization", $"{CoreConstants.Headers.Bearer} {token.Value}");
            }
        }
    }
}
Program.cs:
var baseUrl = builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"];
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes").Get<List<string>>();
builder.Services.AddGraphClient(baseUrl, scopes);
appsettings.json:
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": [ "user.read" ]
  }

Breaking changes Authentication in WebAssembly apps

詳細:

image

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo(
            $"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
    }
}

👇

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Options

@inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> Options
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateToLogin(Options.Get(Microsoft.Extensions.Options.Options.DefaultName).AuthenticationPaths.LogInPath);
    }
}

Run Blazor Web Assembly locally

dotnet publish .\src\blazorwasm-standalone-singleOrg\ -c release -o output
rm output/wwwroot/_framework/Microsoft.Graph.wasm
dotnet serve -o -S -p:7117 -b -d:.\output\wwwroot

Brotli Compression

  • ASP.NET Core Blazor WebAssembly のホストと展開 | Microsoft Learn
  • wwwroot\js\decode.jswwwroot\js\decode.min.js
  • *.csproj
      <PropertyGroup>
        <BlazorEnableCompression>true</BlazorEnableCompression>
      </PropertyGroup>
    
      <ItemGroup>
        <Content Update="wwwroot\js\decode.js">
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        </Content>
      </ItemGroup>
  • wwwroot\index.html
    <body>
    
        <!-- 👇 ここから -->
        <!-- NOTE: https://learn.microsoft.com/ja-jp/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-8.0#compression -->
        <script src="_framework/blazor.webassembly.js" autostart="false"></script>
        <script type="module">
            import { BrotliDecode } from './js/decode.min.js';
            Blazor.start({
            loadBootResource: function (type, name, defaultUri, integrity) {
                if (type !== 'dotnetjs' && location.hostname !== 'localhost' && type !== 'configuration') {
                return (async function () {
                    const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
                    if (!response.ok) {
                    throw new Error(response.statusText);
                    }
                    const originalResponseBuffer = await response.arrayBuffer();
                    const originalResponseArray = new Int8Array(originalResponseBuffer);
                    const decompressedResponseArray = BrotliDecode(originalResponseArray);
                    const contentType = type === 
                    'dotnetwasm' ? 'application/wasm' : 'application/octet-stream';
                    return new Response(decompressedResponseArray, 
                    { headers: { 'content-type': contentType } });
                })();
                }
            }
            });
        </script>
        <!-- 👆 ここまで -->
    </body>