🔐

Blazor WebAssemblyとAuth0でユーザー認証

2021/08/15に公開

Blazor WebAssemblyでシンプルなユーザー認証をすることがあり、Auth0を使った方法が簡単でよかったです。

以下のエントリーを参考にしました。以下のエントリーはAPIの保護に関する説明のためにBlazor Serverのコードも含んでいますが、私の場合はAPIは他のクラウドサービスを利用しているため、Blazor WebAssemblyのみで構成しました。

https://auth0.com/blog/securing-blazor-webassembly-apps/

プロジェクトの作成

Visual StudioでBlazor WebAssemblyのプロジェクトを作成します。以下のサンプルコードではプロジェクト名にBlazorAuthを指定した際のコードになりますので、サンプルコードのBlazorAuthの部分はプロジェクトの名前に応じて変更してください。

ユーザー認証機能を搭載しますが、「認証の種類」には「なし」を選択します。

Auth0にアプリケーションを登録

Auth0にログインしてアプリケーションを登録します。

https://auth0.com/

  • Application TypeにSingle Page Applicationを選択
  • Allowed Callback URLsにhttps://localhost:5001/authentication/login-callbackを追加
  • Allowed Logout URLsにhttps://localhost:5001を追加

Auth0の管理画面のSettings > LanguagesでJapanese (ja)をチェックすると認証画面が日本語に対応します。

Blazor WebAssemblyにコードを追加

wwwroot/appsettings.json

wwwrootのフォルダにappsettings.jsonを作成して以下のように記述します。

<YOUR_AUTH0_DOMAIN>にはAuth0で登録したアプリケーションのドメイン、<YOUR_CLIENT_ID>はAuth0で登録したアプリケーションのクライアントIDを指定します。

wwwroot/appsettings.json
{
  "Auth0": {
    "Authority": "https://<YOUR_AUTH0_DOMAIN>",
    "ClientId": "<YOUR_CLIENT_ID>"
  }
}

認証パッケージの追加

Visual Studioのツールバーのプロジェクト > NuGet パッケージの管理をクリックしてパッケージの管理画面を開き、Microsoft.AspNetCore.Components.WebAssembly.Authenticationをインストールします。

Program.cs

Program.cs
using BlazorAuth;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

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

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddOidcAuthentication(options => {
    builder.Configuration.Bind("Auth0", options.ProviderOptions);
    options.ProviderOptions.ResponseType = "code";
});

await builder.Build().RunAsync();

wwwroot/index.html

wwwroot/index.htmlに認証に関するJavascriptを読み込むように編集します。blazor.webassembly.jsを読み込むタグの前にAuthenticationService.jsを読み込むタグを追加します。

wwwroot/index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorAuth</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorAuth.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

_Imports.razor

_Imports.razorに以下を追加します。

_Imports.razor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

App.razor

App.razor
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <p>Determining session state, please wait...</p>
                </Authorizing>
                <NotAuthorized>
                    <h1>Sorry</h1>
                    <p>You're not authorized to reach this page. You need to log in.</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Shared/AccessControl.razor

Shared/AccessControl.razor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <a href="#" @onclick="BeginSignOut">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Shared/MainLayout.razor

Shared/MainLayout.razor
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <AccessControl />
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

Pages/Authentication.razor

Pages/Authentication.razor
@page "/authentication/{action}"

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Configuration

@inject NavigationManager Navigation
@inject IConfiguration Configuration

<RemoteAuthenticatorView Action="@Action">
    <LogOut>
        @{
            var authority = (string)Configuration["Auth0:Authority"];
            var clientId = (string)Configuration["Auth0:ClientId"];

             Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
        }
    </LogOut>
</RemoteAuthenticatorView>

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

ページをユーザー認証で保護する場合

@attribute [Authorize]を追加することでページをユーザー認証で保護することができます。例えばFetchData.razorのページを保護するためには以下のように記述します。

Pages/FetchData.razor
@page "/fetchdata"

@attribute [Authorize]

@inject HttpClient Http

<h1>Weather forecast</h1>

...

動作確認

動作例

トップページにログインのリンクが表示されています。

ログインのリンクをクリックするとAuth0のログイン画面が開きます。

Auth0でログインすると、さきほどまでログインのリンクがあった場所にユーザー名とログアウトのリンクが表示されました。

未解決の問題

開発環境で動作中にAuth0のログイン画面が開いた際にWebブラウザがクラッシュすることがあります。以下のようなエラーメッセージとなっていました。

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

実際の運用でこのエラーが発生するか、解決する方法などは今後検証していきたいと思います。

まとめ

Auth0を利用してBlazor WebAssemblyにユーザー認証を追加することができました。Blazor WebAssemblyとAuth0の組み合わせはユーザー認証を実現する方法として簡単でよかったです。

Auth0はいつか使いたいと思ったまま使う機会が無かったのですが、Blazor WebAssemblyのおかげで使うことができそうです。

Discussion