BlazorWebAssemblyでFirebase Authenticationを使う

なかなか見つからなかった

サンプルを探しても.net 3.0時代の古い情報ばかりでなかなか見つからず、移植作業もうまくいかなくて1か月近くネットをさまよっていたら日本マイクロソフトの社員さんが作っているZennの記事を見つけてようやく実装することができました。

ASP.NET Core Blazor Server でオレオレ認証を追加したい without Cookie
https://zenn.dev/okazuki/articles/blazor-oreore-auth-part3

必要なコンポーネントをインストールする

  • Microsoft.AspNetCore.Components.WebAssembly.Authentication
  • FirebaseAuthentication.net

Microsoft.AspNetCore.Components.WebAssembly.Authenticationが認証機能を追加するコンポーネントで、FirebaseAuthentication.netはFirebase Autheticationを使うの必要なコンポーネントです。

カスタム認証用のプロバイダを作る

マイクロソフトにあるドキュメントではAzureのActiveDirectoryだと設定だけでできたり、Identity Serverを使ったサンプルは載っているのですが独自認証をしたい場合のことがほんのちょっとしか書いておらずなおかつ英語でもやっている方を見つけるのができなかったので記事を見つけるまではすごく苦労していました。

記事を参考に認証用のプロバイダを作っていきます。AuthenticationStateProviderが認証を管理しているプロバイダいなるのでこれを継承したクラスを作ります。
プロバイダの部分は特に変更なく記事通りに作りました。

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    /// <summary>
    /// 未認証ステータスです。
    /// </summary>
    private static readonly AuthenticationState UnauthorizedAuthenticationState = new AuthenticationState(new ClaimsPrincipal());

    /// <summary>
    /// 認証情報です。
    /// </summary>
    private ClaimsPrincipal? _principal;

    /// <summary>
    /// 認証状態を返します。
    /// </summary>
    /// <returns>認証状態を返します。</returns>
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        if (_principal is null) return Task.FromResult(UnauthorizedAuthenticationState);
        return Task.FromResult(new AuthenticationState(_principal));
    }

    /// <summary>
    /// ステータスの更新を行います。
    /// </summary>
    /// <param name="principal">認証情報を指定します。</param>
    /// <returns></returns>
    public Task UpdateSignInStatusAsync(ClaimsPrincipal? principal)
    {
        _principal = principal;
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        return Task.CompletedTask;
    }
}

認証用のサービスを作成

Firebaseでの認証をするためのサービスの追加を行います。
コンポーネント自体はメールアドレス以外の認証方法でも対応していますが自分が使っているのはメールアドレス認証のみなのでそちらの実装しかしていません。そのうち実装したいなぁ・・・。

public class FirebaseAuthService
{
    private readonly FirebaseAuthConfig config;

    public FirebaseAuthService(FirebaseSettings firebaseSettings)
    {
        config = new FirebaseAuthConfig
        {
            ApiKey = firebaseSettings.FirebaseApiKey,
            AuthDomain = firebaseSettings.FirebaseAuthDomain,
            Providers = new FirebaseAuthProvider[]
            {
                new EmailProvider()
            }
        };
    }

    /// <summary>
    /// メールアドレスを使ってサインインします。
    /// </summary>
    /// <param name="email"></param>
    /// <param name="password"></param>
    public async Task<UserCredential?> SignInWithEmail(string? email, string? password)
    {
        UserCredential? userCredential = null;

        try
        {
            FirebaseAuthClient client = new FirebaseAuthClient(config);

            FetchUserProvidersResult result = await client.FetchSignInMethodsForEmailAsync(email);

            if (result.UserExists && result.AllProviders.Contains(FirebaseProviderType.EmailAndPassword))
            {
                userCredential = await client.SignInWithEmailAndPasswordAsync(email, password);
            }
        }
        catch (FirebaseAuthException ex)
        {
            Console.WriteLine(ex);
            throw;
        }

        return userCredential;
    }

    /// <summary>
    /// サインアウトします。
    /// </summary>
    public void Signout()
    {
        FirebaseAuthClient firebaseAuthClient = new FirebaseAuthClient(config);
        firebaseAuthClient.SignOut();
    }
}

設定ファイル読み込み

FirebaseのAPI情報はappsettings.jsonに保存して呼び出すようにしました。このファイルどこにあるんだろうと思ったらBlazorプロジェクトはwwwrootディレクトリにあるんでですね。

{
  "FirebaseApiKey": "<Your API Key>",
  "FirebaseAuthDomain": "<Your app domain>"
}

設定用のモデルクラスを作成しておきます。

/// <summary>
/// Firebase設定情報のモデルです。
/// </summary>
public class FirebaseSettings
{
    /// <summary>
    /// APIキーを取得/設定します。
    /// </summary>
    public string? FirebaseApiKey { get; set; }

    /// <summary>
    /// ドメインを取得/設定します。
    /// </summary>
    public string? FirebaseAuthDomain { get; set; }

    public FirebaseSettings()
    {
        FirebaseApiKey = string.Empty;
        FirebaseAuthDomain = string.Empty;
    }
}

DIしたいのでProgram.csに注入部分を記述します。この部分、調べるとnullチェックなしで記述してる人が多かったんですがチェッカーがWarning出すのでnullチェックする書き方になりました。

builder.Services.AddScoped(p => {
    FirebaseSettings? firebaseSettings = p.GetRequiredService<IConfiguration>().Get<FirebaseSettings>();

    if(firebaseSettings == null)
    {
        firebaseSettings = new FirebaseSettings();
    }

    return firebaseSettings;
});

ログインを実装

App.razorの中身をCascadingAuthenticationStateコンポーネントで囲うようにします。こうしないと認証が使えません。
AuthorizeRouteViewコンポーネントはRouteViewに認証状態を保持AuthorizeViewコンポーネントを組み合わせたコンポーネントです。
未ログイン時にリダイレクトさせるRedirectToLoginコンポーネントはあとで作成します。

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

sharedディレクトリにRedirectToLogin.razorを作成してリダイレクトを行います。

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager NavigationManager

@code {
    protected override void OnInitialized()
    {
        NavigationManager.NavigateTo("/");
    }
}

ログインフォームの実装を行います。
<Authorized>で囲っている部分が認証されている場合に表示される部分で、<NotAuthorized>で囲っている部分が未認証時に表示される部分になります。

<AuthorizeView>
    <Authorized>
        <h1>ようこそ @context.User.Identity?.Name さん</h1>
        <button @onclick="SignOut">ログアウト</button>
    </Authorized>
    <NotAuthorized>
        <h1>ログイン画面</h1>
        <div>
            <label>email</label>
            <input @bind="_email" />
        </div>
        <div>
            <label>password</label>
            <input type="password" @bind="_password" />
        </div>
        <div>
            <button type="submit" @onclick="SignIn">ログイン</button>
        </div>

    </NotAuthorized>
</AuthorizeView>

コードブロックはこんな感じ。
注入したプロバイダとFirebase設定情報を@injectで呼び出しておきます。

@inject CustomAuthStateProvider _authProvider
@inject FirebaseSettings _firebaseSettings
@code {
    private string? _email;
    private string? _password;
    private async Task SignIn()
    {
        if (string.IsNullOrWhiteSpace(_email)) return;

        FirebaseAuthService firebaseAuthService = new FirebaseAuthService(_firebaseSettings);
        UserCredential? userCredential = await firebaseAuthService.SignInWithEmail(_email, _password);

        if (userCredential != null)
        {
            // ログインができたらプロバイダでステータスを更新する
            await _authProvider.UpdateSignInStatusAsync(new ClaimsPrincipal(
                new ClaimsIdentity(
                    new Claim[]
                    {
                    new (ClaimTypes.Name, _email),
                    },
                    "Custom"
                )
            ));
        }
    }

    private async Task SignOut()
    {
        await _authProvider.UpdateSignInStatusAsync(null);
    }
}

これでログイン部分は実装できると思います。Githubに実装したプロジェクトを上げてます。

BlazorWebAssemblyApp-Auth-Sample
https://github.com/rtssn/BlazorWebAssemblyApp-Auth-Sample

2024/03/05追記
結局、うまくいかないので諦めました・・・。ASP.net identityを使った方が簡単だと思います。