mcpField
D
dotnet-blazor-auth

dotnet-blazor-auth

github.com/wshaddixセキュリティ8685 回インストール35 週間前に更新
リポジトリ

dotnet-blazor-auth について

各ホスティングモデルの Blazor アプリで認証と認可を実装するためのガイドです。認証 UI、Identity のスキャフォールディング、ポリシー、Cookie とトークンのフローを扱います。

主な機能

  1. 01ホスティングモデル別の認証フロー
  2. 02AuthorizeView と認証状態
  3. 03Identity UI のスキャフォールディング
  4. 04ロール・ポリシーによる認可
  5. 05外部 ID プロバイダー
  6. 06トークン処理の指針

活用シーン

  1. 01Blazor アプリにログイン・ログアウト画面を追加
  2. 02ロールやポリシーでページへのアクセスを制限
  3. 03Server、WebAssembly、Hybrid の認証を設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-blazor-auth

インストール後、Claude Code で/dotnet-blazor-authを入力して読み込まれたか確認します。

詳細

dotnet-blazor-auth

すべての Blazor ホスティングモデルにおける認証と認可を扱います。AuthorizeView、CascadingAuthenticationState、Identity UI のスキャフォールディング、ロール/ポリシーベースの認可、ホスティングモデルごとの認証フローの違い(Cookie とトークン)、外部 ID プロバイダーを取り上げます。

対象範囲の境界: このスキルが扱うのは、Blazor 固有の認証 UI パターン(AuthorizeView、CascadingAuthenticationState、Identity UI のスキャフォールディング、クライアント側のトークン処理、ホスティングモデルごとの認証フロー構成)です。API レベルの認証(JWT、OAuth/OIDC、パスキー、CORS、レート制限)については、[skill:dotnet-api-security] を参照してください。

対象外: JWT トークンの生成と検証については、[skill:dotnet-api-security] を参照してください。OWASP のセキュリティ原則については、[skill:dotnet-security-owasp] を参照してください。認証コンポーネントの bUnit テストについては、[skill:dotnet-blazor-testing] を参照してください。E2E 認証テストについては、[skill:dotnet-playwright] を参照してください。UI フレームワークの選定については、[skill:dotnet-ui-chooser] を参照してください。

相互参照:API レベルの認証については [skill:dotnet-api-security]、OWASP の原則については [skill:dotnet-security-owasp]、ホスティングモデルについては [skill:dotnet-blazor-patterns]、コンポーネントアーキテクチャについては [skill:dotnet-blazor-components]、bUnit テストについては [skill:dotnet-blazor-testing]、E2E テストについては [skill:dotnet-playwright]、フレームワークの選定については [skill:dotnet-ui-chooser] を参照してください。


ホスティングモデルごとの認証フロー

認証パターンは Blazor のホスティングモデルによって大きく異なります。

懸念事項InteractiveServerInteractiveWebAssemblyInteractiveAutoStatic SSRHybrid
認証方式Cookie ベース(サーバー側)トークンベース(JWT/OIDC)Cookie(Server フェーズ)、トークン(WASM フェーズ)Cookie ベース(標準の ASP.NET Core)プラットフォームネイティブまたは Cookie
ユーザー状態へのアクセスHttpContext に直接アクセスAuthenticationStateProviderフェーズによって異なりますHttpContextプラットフォームの認証 API
トークンの保存先不要(Cookie)localStorage または sessionStorageCookie からトークンへ移行不要(Cookie)セキュアストレージ(Keychain など)
更新処理回線の再接続インターセプターによるトークン更新自動標準の Cookie 更新プラットフォーム固有
InteractiveServer の認証

サーバーサイド Blazor では Cookie 認証を使用します。ユーザーは標準の ASP.NET Core ログインフローで認証され、SignalR 回線を確立する最初の HTTP リクエストとともに Cookie が送信されます。

// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.AccessDeniedPath = "/Account/AccessDenied";
    });

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();

注意点: HttpContext は最初の HTTP リクエスト中には利用できますが、SignalR 回線の確立後はインタラクティブコンポーネント内で null になります。インタラクティブコンポーネントのライフサイクルメソッド内で HttpContext にアクセスしないでください。代わりに AuthenticationStateProvider を使用してください。

InteractiveWebAssembly の認証

WASM はブラウザーで実行されます。同一オリジンの API(および Backend-for-Frontend / BFF パターン)では Cookie 認証を使用できますが、クロスオリジン API や委任アクセスのシナリオでは、トークンベース認証(OIDC/JWT)が標準的な方法です。

// Client Program.cs (WASM)
builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = "https://login.example.com";
    options.ProviderOptions.ClientId = "blazor-wasm-client";
    options.ProviderOptions.ResponseType = "code";
    options.ProviderOptions.DefaultScopes.Add("api");
});
// Attach tokens to API calls using BaseAddressAuthorizationMessageHandler
// (auto-attaches tokens for requests to the app's base address)
builder.Services.AddHttpClient("API", client =>
    client.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler(sp =>
        sp.GetRequiredService<AuthorizationMessageHandler>()
            .ConfigureHandler(
                authorizedUrls: ["https://api.example.com"],
                scopes: ["api"]));

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

Auto モードは InteractiveServer(Cookie 認証)として開始し、その後 WASM(トークン認証)に移行します。両方に対応してください。

// Server Program.cs
builder.Services.AddAuthentication()
    .AddCookie()
    .AddJwtBearer(); // For WASM API calls after transition

builder.Services.AddCascadingAuthenticationState();
Hybrid(MAUI)の認証
// Register platform-specific auth
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, MauiAuthStateProvider>();

// Custom provider using secure storage
public class MauiAuthStateProvider : AuthenticationStateProvider
{
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await SecureStorage.Default.GetAsync("auth_token");
        if (string.IsNullOrEmpty(token))
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }

        var claims = ParseClaimsFromJwt(token);
        var identity = new ClaimsIdentity(claims, "jwt");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }
}

AuthorizeView

AuthorizeView は、ユーザーの認証状態と認可状態に基づいてコンテンツを条件付きで表示します。

基本的な使い方
<AuthorizeView>
    <Authorized>
        <p>Welcome, @context.User.Identity?.Name!</p>
        <a href="/Account/Logout">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="/Account/Login">Log in</a>
    </NotAuthorized>
    <Authorizing>
        <p>Checking authentication...</p>
    </Authorizing>
</AuthorizeView>
ロールベース
<AuthorizeView Roles="Admin,Manager">
    <Authorized>
        <AdminDashboard />
    </Authorized>
    <NotAuthorized>
        <p>You do not have access to the admin dashboard.</p>
    </NotAuthorized>
</AuthorizeView>
ポリシーベース
<AuthorizeView Policy="CanEditProducts">
    <Authorized>
        <button @onclick="EditProduct">Edit</button>
    </Authorized>
</AuthorizeView>
// Register policy in Program.cs
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("CanEditProducts", policy =>
        policy.RequireClaim("permission", "products.edit"));

CascadingAuthenticationState

CascadingAuthenticationState は、現在の AuthenticationState をカスケードパラメーターとして、すべての子孫コンポーネントに提供します。

セットアップ
// Program.cs -- register cascading auth state
builder.Services.AddCascadingAuthenticationState();

これは、アプリ全体を <CascadingAuthenticationState> でラップする方法(以前のパターン)に代わるものです。サービスベースの登録(.NET 8 以降)が推奨されます。

コンポーネントでの認証状態の利用
@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    private string? userName;

    protected override async Task OnInitializedAsync()
    {
        if (AuthState is not null)
        {
            var state = await AuthState;
            userName = state.User.Identity?.Name;
        }
    }
}
クレームへのアクセス
var state = await AuthState;
var user = state.User;

// Check authentication
if (user.Identity?.IsAuthenticated == true)
{
    var email = user.FindFirst(ClaimTypes.Email)?.Value;
    var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value);
    var isAdmin = user.IsInRole("Admin");
}

Identity UI のスキャフォールディング

ASP.NET Core Identity は、登録、ログイン、メール確認、パスワードのリセット、二要素認証を備えた、完全な認証システムを提供します。

Blazor Web アプリへの Identity の追加
# Add Identity scaffolding
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireNonAlphanumeric = true;
    options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
Identity ページのスキャフォールディング
# Scaffold individual Identity pages for customization
dotnet aspnet-codegenerator identity -dc ApplicationDbContext --files "Account.Login;Account.Register;Account.Logout"
Blazor コンポーネントによるカスタム Identity UI

Blazor ネイティブの認証エクスペリエンスを実現するには、Identity API を呼び出す Blazor コンポーネントを作成します。

@page "/Account/Login"
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager Navigation

<EditForm Model="loginModel" OnValidSubmit="HandleLogin" FormName="login" Enhance>
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div>
        <InputText @bind-Value="loginModel.Email" placeholder="Email" />
    </div>
    <div>
        <InputText @bind-Value="loginModel.Password" type="password" placeholder="Password" />
    </div>
    <div>
        <InputCheckbox @bind-Value="loginModel.RememberMe" /> Remember me
    </div>

    <button type="submit">Log in</button>
</EditForm>

@if (!string.IsNullOrEmpty(errorMessage))
{
    <p class="text-danger">@errorMessage</p>
}

@code {
    [SupplyParameterFromForm]
    private LoginModel loginModel { get; set; } = new();

    private string? errorMessage;

    private async Task HandleLogin()
    {
        var result = await SignInManager.PasswordSignInAsync(
            loginModel.Email, loginModel.Password,
            loginModel.RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            Navigation.NavigateTo("/", forceLoad: true);
        }
        else if (result.RequiresTwoFactor)
        {
            Navigation.NavigateTo("/Account/LoginWith2fa");
        }
        else if (result.IsLockedOut)
        {
            errorMessage = "Account is locked. Try again later.";
        }
        else
        {
            errorMessage = "Invalid login attempt.";
        }
    }
}

注意点: SignInManager は Cookie を設定するために HttpContext を使用します。Interactive レンダリングモードでは、回線が確立された後に HttpContext を使用できません。ログイン/ログアウトページでは、Cookie 操作に必要な HttpContext にアクセスできるよう、Static SSR(@rendermode なし)を使用する必要があります。


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

ページレベルの認可
@page "/admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Panel</h1>
@page "/products/manage"
@attribute [Authorize(Policy = "ProductManager")]

<h1>Manage Products</h1>
ポリシーの定義
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ProductManager", policy =>
        policy.RequireRole("Admin", "ProductManager"))
    .AddPolicy("CanDeleteOrders", policy =>
        policy.RequireClaim("permission", "orders.delete")
              .RequireAuthenticatedUser())
    .AddPolicy("MinimumAge", policy =>
        policy.AddRequirements(new MinimumAgeRequirement(18)));
カスタム認可ハンドラー
public sealed class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
    public int MinimumAge { get; } = minimumAge;
}

public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("date_of_birth");
        if (dateOfBirthClaim is not null
            && DateOnly.TryParse(dateOfBirthClaim.Value, out var dob))
        {
            var age = DateOnly.FromDateTime(DateTime.UtcNow).Year - dob.Year;
            if (age >= requirement.MinimumAge)
            {
                context.Succeed(requirement);
            }
        }
        return Task.CompletedTask;
    }
}

// Register
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
コンポーネントでの手続き型認可
@inject IAuthorizationService AuthorizationService

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    private bool canEdit;

    protected override async Task OnInitializedAsync()
    {
        if (AuthState is not null)
        {
            var state = await AuthState;
            var result = await AuthorizationService.AuthorizeAsync(
                state.User, "CanEditProducts");
            canEdit = result.Succeeded;
        }
    }
}

外部 ID プロバイダー

外部プロバイダーの追加
builder.Services.AddAuthentication()
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
    })
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
    });
ホスティングモデルごとの外部ログインフロー
ホスティングモデルフロー備考
InteractiveServer / Static SSR標準的な OAuth リダイレクト(サーバー側)コールバック後に Cookie を保存
InteractiveWebAssemblyPKCE を使用した OIDC(クライアント側)ブラウザーにトークンを保存
ハイブリッド(MAUI)WebAuthenticator または MSALプラットフォーム固有のセキュアストレージ

WASM では、クライアントプロジェクトで OIDC プロバイダーを構成します。

// Client Program.cs
builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = "https://login.microsoftonline.com/{tenant}";
    options.ProviderOptions.ClientId = "{client-id}";
    options.ProviderOptions.ResponseType = "code";
});

MAUI ハイブリッドの場合:

var result = await WebAuthenticator.Default.AuthenticateAsync(
    new Uri("https://login.example.com/authorize"),
    new Uri("myapp://callback"));
var token = result.AccessToken;

エージェント利用時の注意点

  1. インタラクティブコンポーネントで HttpContext にアクセスしないでください。 HttpContext を使用できるのは、最初の HTTP リクエスト中のみです。SignalR 回線が確立された後(InteractiveServer)、または WASM ランタイムが読み込まれた後は、null になります。代わりに AuthenticationStateProvider または CascadingAuthenticationState を使用してください。
  2. WASM でクロスオリジン API アクセスや委任された API アクセスに Cookie を使用しないでください。 クロスオリジン API には、AuthorizationMessageHandler と OIDC/JWT を使用してください。同一オリジンおよび Backend-for-Frontend(BFF)の Cookie 認証は、引き続き WASM アプリで使用できます。
  3. ログイン/ログアウトページを Interactive モードでレンダリングしないでください。 SignInManager は Cookie の設定や削除に HttpContext を必要とします。ログインページとログアウトページでは、Static SSR レンダリングモードを使用する必要があります。
  4. XSS のリスクを考慮せずに localStorage にトークンを保存しないでください。 アプリに XSS の脆弱性があると、localStorage 内のトークンが盗まれる可能性があります。sessionStorage(タブを閉じると消去されます)、または PKCE と組み合わせた OIDC ライブラリの組み込みストレージ機構を使用してください。
  5. AddCascadingAuthenticationState() を忘れないでください。 これがないと、コンポーネントの [CascadingParameter] Task<AuthenticationState> は常に null となり、認証チェックが気付かれないまま機能しなくなります。
  6. AddIdentity と AddDefaultIdentity を同時に使用しないでください。 AddDefaultIdentity には UI のスキャフォールディングが含まれますが、AddIdentity には含まれません。既定の Identity UI ページを使用するかどうかに応じて、どちらかを選択してください。

前提条件

  • .NET 8.0 以降(レンダリングモードを備えた Blazor Web アプリ、および AddCascadingAuthenticationState サービスの登録)
  • EF Core と Identity を使用するための Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • 既定の Identity UI のスキャフォールディング用の Microsoft.AspNetCore.Identity.UI
  • 外部プロバイダー用の Microsoft.AspNetCore.Authentication.MicrosoftAccount / .Google
  • Microsoft Identity(Azure AD/Entra)を使用する WASM 用の Microsoft.Authentication.WebAssembly.Msal

参考資料

コメント