Blazor での認証と承認: 実践ガイド

· 7分で読める

ASP.NET MVC または Razor Pages を使用したことがある場合は、おそらく認証の仕組みについてのメンタル モデルを持っているでしょう。ミドルウェアが要求をインターセプトし、Cookie またはトークンをチェックし、HttpContext.User を設定して、レースに出発します。 Blazor は、微妙ではあるが重要な方法でそのメンタル モデルを変更します。これらの違いを早い段階で理解していないと、不可能と思われる認証の問題をデバッグすることになります。

この投稿では、サーバー ホスティング モデルと WebAssembly ホスティング モデルの両方をカバーしながら、Blazor で認証と承認が実際にどのように機能するかを説明したいと思います。基本的なことから、カスタム プロバイダー、外部 OAuth、そして経験豊富な .NET 開発者さえも陥る落とし穴までを見ていきます。

Blazor の認証が異なる理由

従来の ASP.NET では、すべてのユーザー操作は HTTP リクエストです。サーバーは資格情報を検証し、Cookie を設定し、後続のすべてのリクエストにその Cookie が含まれます。認証パイプラインは線形で予測可能です。

Blazor サーバーは、永続的な SignalR 接続を介して動作します。最初の HTTP リクエストでページが読み込まれた後、それ以降のすべての対話は WebSocket 経由で行われます。ボタンがクリックされるたびに新しい HTTP リクエストが送信されることはないため、対話のたびにミドルウェアが再実行されることはありません。 HttpContext は最初の接続中に使用できますが、回路の存続期間全体を通してこれに依存するとバグが発生します。

Blazor WebAssembly は完全にブラウザー内で実行されます。サーバー側 HttpContext はまったくありません。認証状態は API から取得し、クライアント側に保存し、トークン (通常は JWT) を通じて管理する必要があります。サーバーは、トークンの検証が行われる限りにおいてのみクライアントを信頼します。

これは、Blazor には、ホスティング モデルに関係なく機能する認証状態用の独自の抽象化が必要であることを意味します。その抽象化は AuthenticationStateProvider です。

認証状態: 財団

Blazor の認証システムの中心となるのは AuthenticationStateProvider です。これは、単一の重要なメソッドを公開する抽象クラスです。

public abstract Task<AuthenticationState> GetAuthenticationStateAsync();

AuthenticationState オブジェクトは ClaimsPrincipal (.NET 全体で使用されるのと同じ ID モデル) をラップします。コンポーネントは Cookie やトークンと直接通信しません。彼らは AuthenticationStateProvider に現在の状態を尋ねます。

この状態をコンポーネント ツリー全体で利用できるようにするために、Blazor は CascadingAuthenticationState を提供します。通常、ルーターを App.razor またはレイアウトでラップします。

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>You're not authorized to view this page.</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Page not found.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

AuthorizeRouteView はここで 2 つの役割を果たしています。一致したページ コンポーネントをレンダリングする前にユーザーが認証および許可されているかどうかをチェックし、そうでない場合にはフォールバック UI を提供します。

統合 Blazor モデルを使用する .NET 8 以降では、これを App.razor で構成すると、サービス登録で AddCascadingAuthenticationState() を使用するときにフレームワークがカスケード パラメーターを自動的に処理します。

ASP.NET ID の統合

ほとんどのプロジェクトでは、認証を最初から構築する必要はありません。 ASP.NET Identity を使用すると、すぐに使えるユーザー管理、パスワード ハッシュ、2 要素認証、アカウント確認が可能になります。Blazor を使用したセットアップは Program.cs で始まります。

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    });

.NET 8 以降の Blazor Web App テンプレートでは、スキャフォールディングされた Identity UI が Razor コンポーネントを直接使用します。ログイン、登録、およびアカウント管理ページは、Blazor アプリケーションの他の部分と自然に統合されます。Razor ページと Blazor コンポーネントをぎこちなく組み合わせる必要はもうありません。

ApplicationDbContextIdentityDbContext から継承しているため、移行を実行して ID テーブルを作成する必要があります。

public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}
dotnet ef migrations add InitialIdentity
dotnet ef database update

AuthorizeView コンポーネント

認証状態がコンポーネント ツリーを通過すると、AuthorizeView を使用して条件付きで UI をレンダリングできます。

<AuthorizeView>
    <Authorized>
        <p>Welcome, @context.User.Identity?.Name!</p>
        <a href="/account/manage">Manage Account</a>
        <form method="post" action="/account/logout">
            <button type="submit">Log Out</button>
        </form>
    </Authorized>
    <NotAuthorized>
        <a href="/account/login">Log In</a>
        <a href="/account/register">Register</a>
    </NotAuthorized>
</AuthorizeView>

<Authorized> 内の context パラメーターを使用すると AuthenticationState にアクセスできるため、マークアップ内でクレーム、ロール、ユーザーの ID を直接検査できます。

AuthorizeView をロールとポリシーで使用することもできます。

<AuthorizeView Roles="Admin,Moderator">
    <Authorized>
        <button @onclick="DeletePost">Delete Post</button>
    </Authorized>
</AuthorizeView>

<AuthorizeView Policy="CanEditArticles">
    <Authorized>
        <button @onclick="EditArticle">Edit</button>
    </Authorized>
</AuthorizeView>

留意すべき点が 1 つあります。AuthorizeView は UI の問題です。要素を非表示または表示しますが、基礎となるロジックは保護されません。誰かがあなたの API エンドポイントを呼び出すか、あなたのメソッドを直接呼び出すことができる場合、その人は AuthorizeView を完全にバイパスします。サーバー側でも常に承認を強制します。

[承認] 属性

ページ全体を保護するには、 [Authorize] 属性を適用します。

@page "/admin/dashboard"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Dashboard</h1>
<p>Only administrators can see this page.</p>

認証されていないユーザーがこのページに移動すると、AuthorizeRouteView が開始され、前に定義した <NotAuthorized> テンプレートがレンダリングされます。代わりに、ナビゲーションを使用して NotAuthorized ケースを処理することで、ログイン ページにリダイレクトできます。

<NotAuthorized>
    @if (!context.User.Identity?.IsAuthenticated ?? true)
    {
        <RedirectToLogin />
    }
    else
    {
        <p>You don't have permission to access this page.</p>
    }
</NotAuthorized>

単純な RedirectToLogin コンポーネントは次のようになります。

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var returnUrl = Uri.EscapeDataString(Navigation.Uri);
        Navigation.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
    }
}

ここでは forceLoad: true が重要です。サーバー側の認証ミドルウェアがログイン フローを適切に処理できるように、実際の HTTP ナビゲーションが必要です。

ロールベースおよびポリシーベースの認可

ロールは最も単純なモデルです。ユーザーを「管理者」や「編集者」などのグループに割り当て、メンバーシップを確認します。ただし、ポリシーにより、はるかに柔軟な柔軟性が得られます。

Program.cs にポリシーを登録します:

builder.Services.AddAuthorizationCore(options =>
{
    options.AddPolicy("CanPublish", policy =>
        policy.RequireClaim("Permission", "Publish"));

    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    options.AddPolicy("PremiumUser", policy =>
        policy.RequireRole("Premium")
              .RequireClaim("Subscription", "Active"));
});

カスタム要件にはハンドラーが必要です。

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");

        if (dateOfBirthClaim is null)
            return Task.CompletedTask;

        var dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
        var age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;

        if (age >= requirement.MinimumAge)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

ハンドラーを登録します。

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

コンポーネントでは、動的ロジックが必要な場合に、プログラムで承認をチェックすることもできます。

@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider

@code {
    private bool canPublish;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var result = await AuthorizationService.AuthorizeAsync(
            authState.User, "CanPublish");
        canPublish = result.Succeeded;
    }
}

外部 OAuth プロバイダー

ASP.NET 認証ミドルウェアを使用すると、「Google でサインイン」または「GitHub でサインイン」を簡単にサポートできます。 OAuth フローには HTTP リダイレクトが必要なため、これらはサーバー側で構成されます。

builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
        options.Scope.Add("profile");
    })
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
    })
    .AddGitHub(options =>
    {
        options.ClientId = builder.Configuration["Auth:GitHub:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:GitHub:ClientSecret"]!;
    });

GitHub の場合は、デフォルトの ASP.NET ライブラリに含まれていないため、AspNet.Security.OAuth.GitHub NuGet パッケージが必要になります。

次に、ログイン UI は、外部チャレンジをトリガーするリンクを提供します。

@page "/account/external-login"

<h2>Sign in with an external provider</h2>

<form method="post" action="/api/auth/external-login">
    <button type="submit" name="provider" value="Google">
        Sign in with Google
    </button>
    <button type="submit" name="provider" value="Microsoft">
        Sign in with Microsoft
    </button>
    <button type="submit" name="provider" value="GitHub">
        Sign in with GitHub
    </button>
</form>

API エンドポイントはチャレンジをトリガーし、コールバックを処理します。

app.MapPost("/api/auth/external-login", (string provider, HttpContext context) =>
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/api/auth/external-callback"
    };
    return Results.Challenge(properties, [provider]);
});

Blazor の外部認証には常にページ全体のナビゲーションが必要です。サーバーを経由せずに SignalR 回線または WebAssembly アプリ内で OAuth リダイレクトを完了することはできません。

Blazor WebAssembly のトークンベースの認証Blazor WebAssembly はクライアント上で実行されるため、Cookie ベースの認証は同じようには適用されません。代わりに、通常はメモリに保存され、発信 HTTP リクエストに添付された JWT を使用します。

フレームワークは、トークンを自動的に添付するための AuthorizationMessageHandler を提供します。

builder.Services.AddHttpClient("API",
    client => client.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler<AuthorizationMessageHandler>();

builder.Services.AddScoped(sp =>
    sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));

独自の API に対して認証するスタンドアロン Blazor WASM アプリの場合は、JWT を解析するカスタム AuthenticationStateProvider を実装します。

public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly ILocalStorageService _localStorage;
    private readonly HttpClient _httpClient;

    public JwtAuthenticationStateProvider(
        ILocalStorageService localStorage,
        HttpClient httpClient)
    {
        _localStorage = localStorage;
        _httpClient = httpClient;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _localStorage.GetItemAsync<string>("authToken");

        if (string.IsNullOrWhiteSpace(token))
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));

        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var claims = ParseClaimsFromJwt(token);
        var identity = new ClaimsIdentity(claims, "jwt");

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void NotifyAuthStateChanged()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        var payload = jwt.Split('.')[1];

        var padded = payload.Length % 4 switch
        {
            2 => payload + "==",
            3 => payload + "=",
            _ => payload
        };

        var bytes = Convert.FromBase64String(padded);
        var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(bytes);

        return json?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())) 
               ?? Enumerable.Empty<Claim>();
    }
}

ログインに成功したら、トークンを保存し、認証状態を通知します。

public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    private readonly JwtAuthenticationStateProvider _authStateProvider;

    public AuthService(
        HttpClient httpClient,
        ILocalStorageService localStorage,
        AuthenticationStateProvider authStateProvider)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
        _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
    }

    public async Task<bool> LoginAsync(string email, string password)
    {
        var response = await _httpClient.PostAsJsonAsync("/api/auth/login",
            new { Email = email, Password = password });

        if (!response.IsSuccessStatusCode)
            return false;

        var result = await response.Content
            .ReadFromJsonAsync<LoginResponse>();

        await _localStorage.SetItemAsync("authToken", result!.Token);
        _authStateProvider.NotifyAuthStateChanged();

        return true;
    }

    public async Task LogoutAsync()
    {
        await _localStorage.RemoveItemAsync("authToken");
        _authStateProvider.NotifyAuthStateChanged();
    }
}

注意: JWT を localStorage に保存すると、XSS 攻撃にさらされます。よりセキュリティの高いアプリケーションの場合は、トークンをメモリ内にのみ保持してリフレッシュ トークンを使用するか、サーバーがトークンを管理し、クライアントが HTTP のみの Cookie を使用するバックエンド対フロントエンド (BFF) パターンを採用することを検討してください。

Blazor サーバーと WebAssembly: セキュリティに関する考慮事項

ホスティング モデルはセキュリティ体制を根本的に変えます。

Blazor Server は、すべてのコンポーネント ロジックをサーバー上に保持します。クライアントには、SignalR 上でレンダリングされた HTML の差分のみが表示されます。これは次のことを意味します。

  • 機密性の高いロジックがサーバーから離れることはありません
  • コンポーネントからデータベースや内部サービスに直接アクセスできます。
  • 認証状態は、初期接続時のサーバーの HttpContext から取得されます
  • 回線は認証 Cookie よりも存続できます。ユーザーの Cookie の有効期限が切れても、回線は切断されるまで存続します。
  • 回線の切断を適切に処理し、再接続時に認証状態を再検証する必要があります。

Blazor WebAssembly は完全にブラウザー内で実行されます。これは次のことを意味します。

  • すべてのコンポーネント コードはダウンロードして検査可能です
  • WASM コンポーネントにはシークレット、接続文字列、または機密のビジネス ロジックを決して入れないでください。
  • 認証は UX のクライアントにのみ適用されます。実際の強制は API レイヤーで行われる必要があります
  • トークンの管理はあなたの責任です
  • サーバー プロジェクトが認証を処理し、WASM アプリを提供するホスト型モデルの使用を検討します。

WebAssembly アプリに対して推奨するパターンは、すべてのコンポーネントを「信頼できない UI」であるかのように扱い、すべての API エンドポイントを未知のクライアントによって呼び出されているかのように扱うことです。クライアントが何をチェックするかに関係なく、サーバー側ですべてを検証します。

カスタム AuthenticationStateProvider の構築

場合によっては、組み込みプロバイダーがアーキテクチャに適合しないことがあります。おそらく、従来の認証システムと統合しているか、認証状態の変更をポーリングする必要があると考えられます。 Blazor Server のより完全なカスタム プロバイダーは次のとおりです。

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IUserService _userService;

    public CustomAuthStateProvider(
        IHttpContextAccessor httpContextAccessor,
        IUserService userService)
    {
        _httpContextAccessor = httpContextAccessor;
        _userService = userService;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var httpContext = _httpContextAccessor.HttpContext;

        if (httpContext?.User?.Identity?.IsAuthenticated != true)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var user = await _userService.GetUserWithClaimsAsync(userId);

        if (user is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Name, user.DisplayName),
            new(ClaimTypes.Email, user.Email)
        };

        claims.AddRange(user.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
        claims.AddRange(user.Permissions.Select(p => new Claim("Permission", p)));

        var identity = new ClaimsIdentity(claims, "Custom");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void MarkUserAsAuthenticated(string userId)
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void MarkUserAsLoggedOut()
    {
        var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymous));
        NotifyAuthenticationStateChanged(authState);
    }
}

Program.cs に登録します。

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<CustomAuthStateProvider>();

ここでの重要な洞察は NotifyAuthenticationStateChanged です。これを呼び出すと、カスケード パラメーターの更新がトリガーされ、コンポーネント ツリー内のすべての AuthorizeViewAuthorizeRouteView が再評価されます。これは、ページ全体を更新せずに UI をログイン/ログアウト イベントに反応させる方法です。

よくある落とし穴と解決策

多くのプロジェクトで Blazor 認証を使用した後、最も頻繁に発生する問題は次のとおりです。

1. Blazor サーバー コンポーネントでの HttpContext の使用HttpContext は最初の HTTP 要求中に使用できますが、SignalR の対話中は null または無効になります。最初のレンダリング後に実行されるコンポーネントに IHttpContextAccessor を挿入しないでください。

解決策: 初期化中に HttpContext から必要なものを取得し、スコープ指定されたサービスに保存します。

public class UserContext
{
    public string? UserId { get; set; }
    public string? AccessToken { get; set; }
}

// In a component that renders during the initial HTTP request:
@inject IHttpContextAccessor HttpContextAccessor
@inject UserContext UserContext

@code {
    protected override void OnInitialized()
    {
        var context = HttpContextAccessor.HttpContext;
        UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
        UserContext.AccessToken = context?.Request.Headers.Authorization
            .ToString().Replace("Bearer ", "");
    }
}

2. ログイン後に認証状態が更新されない

ログイン API を呼び出すと成功しますが、UI にはまだ「ログイン」と表示されます。

解決策: 認証状態が変更された後、AuthenticationStateProviderNotifyAuthenticationStateChanged を呼び出す必要があります。フレームワークは、トークンが保存されたことや Cookie が設定されたことを魔法のように検出しません。

3. コンポーネントで承認属性が機能しない

[Authorize] をコンポーネントに追加しても、認証されていないユーザーはブロックされません。

解決策: App.razor でプレーン RouteView の代わりに AuthorizeRouteView を使用していることを確認してください。標準 RouteView は認可属性を完全に無視します。

4. プリレンダリングにより認証状態が中断される

Blazor WebAssembly でのサーバー側の事前レンダリング中は、使用可能な認証トークンがありません。コンポーネントは非認証としてレンダリングされ、WASM のロード後に認証された状態に切り替わります。

解決策: @rendermode InteractiveWebAssembly (プリレンダリングなし) を使用して認証に依存するページのプリレンダリングを無効にするか、読み込み状態を適切に処理します。

<AuthorizeView>
    <Authorized>
        <p>Welcome back, @context.User.Identity?.Name</p>
    </Authorized>
    <Authorizing>
        <p>Loading...</p>
    </Authorizing>
    <NotAuthorized>
        <a href="/login">Sign in</a>
    </NotAuthorized>
</AuthorizeView>

5. 長時間実行される回線におけるトークンの有効期限

Blazor サーバー回線は何時間も稼働し続けることができます。トークンまたはセッションの有効期限が切れると、ユーザーは UI で「認証」されたままになりますが、API 呼び出しは失敗し始めます。

解決策: 定期的なチェックを実装するか、RevalidatingServerAuthenticationStateProvider:

public class RevalidatingAuthStateProvider
    : RevalidatingServerAuthenticationStateProvider
{
    private readonly IServiceScopeFactory _scopeFactory;

    public RevalidatingAuthStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory)
        : base(loggerFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var userManager = scope.ServiceProvider
            .GetRequiredService<UserManager<IdentityUser>>();

        var user = await userManager.GetUserAsync(authenticationState.User);
        return user is not null;
    }
}

これにより、ユーザーがまだ存在している (セキュリティ スタンプが変更されていない) ことが 30 分ごとに検証されます。

結論

Blazor での認証と承認には、従来の要求と応答の ASP.NET から考え方を変える必要があります。 AuthenticationStateProvider の抽象化は、すべてがどのように組み合わされるかを理解するための鍵です。これを一度内面化すれば、残りは自然についてきます。

ほとんどのアプリケーションでは、ASP.NET ID と組み込みのテンプレートから始めます。これらは、ユーザー管理、パスワードのハッシュ化、トークン生成といった重労働を処理します。要件の拡大に応じて、ポリシーとクレームベースの承認を追加します。ユーザーが外部 OAuth プロバイダーを期待している場合は、外部 OAuth プロバイダーを追加します。

ホスティング モデルは重要です。Blazor Server は、コードがサーバー上に留まる、より伝統的なセキュリティ体制を提供します。一方、WebAssembly は、クライアントが設計上信頼されていない API ファーストの考え方を推進します。どちらも本質的に安全性が高いわけではなく、脅威モデルが異なるだけです。

どのアプローチを選択する場合でも、次の黄金律を覚えておいてください。UI での承認はユーザー エクスペリエンスのため、サーバーでの承認はセキュリティのためです。 常に両方を強制してください。