認証

【Blazor】Identity Server を使用して認証機能を実装する方法

identity-server-auth

Blazor では、テンプレートを作成する段階で「認証あり」としておくだけで、認証付きアプリのサンプルを作成することができます。

テンプレートとして実装されているのは、「Identity Server」を使用した認証機能です。

Identity Server とは、ASP.NETCore 用に認証まわりの機能を提供するオープンソースのサービスです。

本記事では、認証機能付きプロジェクトの作成方法から、認証なしで API を作成する方法まで解説しました。

これを読めば、認証まわりの基本的な動作が理解できるはずです。

それでは、さっそく見ていきましょう。

認証機能付きプロジェクトの作成

Visual Studio 2019 から新規プロジェクトを作成していきます。

identity-server-auth

Blazor WebAssenbly アプリを選択します。

identity-server-auth

次の設定にします。

  • 対象のフレームワーク:.NET 5.0
  • 認証:個別の認証(アプリ内)
  • 詳細設定:ASP .NET Core でホストされた
identity-server-auth

好きなプロジェクト名をつけて、作成しましょう。

identity-server-auth

Client、Server、Shared の3つのプロジェクトが自動で作成され、同時に認証機能のサンプルも実装されます。

動作を確認するために、デバッグ実行をしてみましょう。

identity-server-auth

ブラウザが立ち上がり、「Authorizing…」が画面に表示されます。

裏で認証済みのユーザーかどうかのチェックをしているわけですね。

identity-server-auth

ホーム画面が表示されました。

認証されていないと判定されたので、ヘッダのところには「Register」と「Log in」のメニューが表示されています。

サンプルプロジェクトでは、「Fetch data」を参照するには認証が必要な仕組みになっているので、試しにクリックしてみましょう。

identity-server-auth

すると、「Checking login state…」が表示され、認証状態をチェックします。

identity-server-auth

認証がされていなかったので、ログインページにリダイレクトされました。

まだユーザーの登録が済んでいないので、「Register as a new user」を選択しましょう。

identity-server-auth

適当なメールアドレスとパスワードを入力して、「Register」を選択します。

かなり複雑なパスワードじゃないと先に進めません。
identity-server-auth
ねこじょーかー
ねこじょーかー
何度もエラーになって、ようやく「$*gK#Lb5w6Ey」で先に進めました…

登録が完了し、メール認証のステップに進みます。

デバッグの場合は、リンクをクリックするだけで認証が完了するので、表示されたリンクをクリックしてください。

identity-server-auth

「Comfirm email」が表示されると、メール認証が完了した証拠です。

「Sample.Server」をクリックして、またホーム画面に移動しましょう。

identity-server-auth

ログインしてもう一度 Fetch data をクリックすると、認証済みとして判定され、無事に画面を表示することができました。

また、ヘッダからもログインやサインインのメニューが消えて、メールアドレスが表示されていることも確認できますね。

identity-server-auth

基本的な流れとしてはこんな感じです。

認証を必要としない API アクセス

ここまで見てきたとおり、Fetch data のページを見るには認証を必要としています。

この認証を外して、ログインしていなくてもページを見れるように修正してみましょう。

まずは、現状がどのような動きになっているのかを見ていきます。

画面側の修正

まずは、画面の UI 部分を実装している FetchData.razor です。

Client.Pages.FetchData.razor
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Sample.Shared
@*@attribute [Authorize]*@ ←ここをコメントアウト!!
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }

}

@attribute [Authorize] という記述がありますね。

これは何かというと、「このページを見るには認証が必要ですよ」という宣言です。

つまり、これを外せば認証が不要になるということですね。

「@**@」を書いてコメントアウトしましょう。

OnInitializedAsyncAccessTokenNotAvailableException をキャッチしていますが、これは「認証せずにページを表示したときに、ログインページにリダイレクトする」という処理を実現するためです。

Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); でサーバー側の API 呼び出しをしているので、ここでエラーになることを想定しています。

サーバー API の修正

次に、API 側を修正していきます。

FetchData.razor で呼び出していたのは、WeatherForecastController.cs の処理です。

Server.Controllers.WeatherForecastController.cs
namespace Sample.Server.Controllers
{
    //[Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {

このクラスにも、[Authorize] のアトリビュートがついていることが確認できますね。

画面側と同じように、「この API を呼び出すためには、認証が必要ですよ」という意味です。

ということで、[Authorize] のアトリビュートはコメントアウトしておきましょう。

これで認証しなくても、Fetch data の画面が表示できるはずです。

動作確認

先ほどと同じようにデバッグ実行をして、Fetch data をクリックしてみます。

すると、「Checking login state…」の画面が表示されて、ログインページにリダイレクトされてしまいました。

identity-server-auth
ペンギンくん
ペンギンくん
あれ?なんでだろ
ねこじょーかー
ねこじょーかー
結構ハマったけど、解決方法があるので解説するね。

API 実行で必ず認証されてしまう原因

原因は、クライアント側の Program.cs にあります。

Sample.Client.Program.cs
namespace Sample.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            // ここ!!
            builder.Services.AddHttpClient("Sample.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("Sample.ServerAPI"));

            builder.Services.AddApiAuthorization();

            await builder.Build().RunAsync();
        }
    }
}

.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>() を書くことで、アクセストークンを API リクエストに追加しなくて済むようになります。

しかし、この処理が原因で HttpClient を使った API リクエストには必ずアクセストークンが必要になり、認証なしで API を実行できません。

ではどうするかというと、認証なし用の HttpClient をもうひとつ作成します

まずは Client 側に、PublicClient というクラスを作成しましょう。

Sample.Client.PublicClient.cs
using System.Net.Http;

namespace Sample.Client
{
    public class PublicClient
    {
        public HttpClient Client { get; }

        public PublicClient(HttpClient httpClient)
        {
            Client = httpClient;
        }
    }
}

そしてもう一度 Program.cs に戻り、認証なし用の HttpClient を追加します。

Sample.Client.Program.cs
namespace Sample.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

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

            // ここを追加!!
            builder.Services.AddHttpClient<PublicClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

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

            builder.Services.AddApiAuthorization();

            await builder.Build().RunAsync();
        }
    }
}

最後に、Http から API を呼んでいた箇所を、先ほど作成した Public.Client から呼び出すように修正します。

PublicClient のクラスが使えるように、上の方で inject の宣言を追加しましょう。

Client.Pages.FetchData.razor
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Sample.Shared
@*@attribute [Authorize]*@
@inject HttpClient Http

@*ここを追加!!*@
@inject PublicClient Public

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            @*Http ではなく Public.Client から API を呼び出す!*@
            forecasts = await Public.Client.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }

}

これで修正は完了です。

ペンギンくん
ペンギンくん
なるほど。修正は難しくないね。

修正後の動作確認

では、実際に認証なしで Fetch data のページが見れるか確認してみましょう。

デバッグ実行してみると、認証無しでページを見ることができました。

ヘッダに「Log in」が表示されているので、認証がされてないことが確認できますね。

identity-server-auth

Controller 側に AllowAnonymous のアトリビュートをつけると「認証が不要な API」という意味になりますが、試しても認証が必要になってしまいました。

今回は使えないようですが、「そんな機能もあるんだな」ということは覚えておきましょう。

最後に

認証機能付きプロジェクトの作成方法から、認証なしで API を作成する方法まで解説しました。

なんか…重くない?

実際に試した人は「重い」と感じるはずです。

オープンソースの仕組みなので仕方がないのかもしれませんが、実際の運用を考えるともう少しサクサク動いてほしいところです。

そこで、「Azure Active Directory B2C」というサービスを使うことで、もっとサクサクと認証処理を動かすことができるようになります。

認証機能を Azure Active Directory B2C で実現する方法で解説しているので、合わせてご覧ください。

Blazor の書籍も好評発売中!
blazor-book

入門編から EC サイトを作る応用編まで、Blazor の本を3冊執筆しました。

私が1年以上かけて学習した内容をすべて詰め込んでいるので、さらにステップアップしたい方はぜひご覧ください。