Blazor WebAssemblyとAuth0でユーザー認証
Blazor WebAssemblyでシンプルなユーザー認証をすることがあり、Auth0を使った方法が簡単でよかったです。
以下のエントリーを参考にしました。以下のエントリーはAPIの保護に関する説明のためにBlazor Serverのコードも含んでいますが、私の場合はAPIは他のクラウドサービスを利用しているため、Blazor WebAssemblyのみで構成しました。
プロジェクトの作成
Visual StudioでBlazor WebAssemblyのプロジェクトを作成します。以下のサンプルコードではプロジェクト名にBlazorAuth
を指定した際のコードになりますので、サンプルコードのBlazorAuth
の部分はプロジェクトの名前に応じて変更してください。
ユーザー認証機能を搭載しますが、「認証の種類」には「なし」を選択します。
Auth0にアプリケーションを登録
Auth0にログインしてアプリケーションを登録します。
- 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を指定します。
{
"Auth0": {
"Authority": "https://<YOUR_AUTH0_DOMAIN>",
"ClientId": "<YOUR_CLIENT_ID>"
}
}
認証パッケージの追加
Visual Studioのツールバーのプロジェクト > NuGet パッケージの管理をクリックしてパッケージの管理画面を開き、Microsoft.AspNetCore.Components.WebAssembly.Authentication
をインストールします。
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
を読み込むタグを追加します。
<!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
に以下を追加します。
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
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
@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
@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
@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
のページを保護するためには以下のように記述します。
@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