ASP.NET Core でのリソース ベースの認可

この記事では、アプリ リソースへのアクセスをユーザーに承認する方法について説明します。

アプリでは、 リソース は通常、コレクションに格納されているデータ ( byte[] 配列など) を含む C# クラスによって表されます。 通常、このクラスには、一意のリソース識別子、日付、作成者、ソース情報、UI に表示するためのフレンドリ名など、リソースに関連する追加のメタデータが含まれます。 リソース データを保持するコレクションは、通常、物理ファイルコンテンツ、クラウド ストレージ オブジェクト、メモリ内オブジェクト、またはデータベースからのデータから読み込まれます。

リソース ベースの承認では、ASP.NET Core アプリで特別な注意が必要です。 属性の評価は、データ バインディングの前と、リソースを読み込むメソッドの実行前に行われます。 [Authorize]属性を使用した宣言型承認では、リソースベースの承認では不十分です。 代わりに、アプリはカスタム承認メソッド ( 命令型承認と呼ばれるアプローチ) を呼び出す必要があります。

この記事では、Razor コンポーネントの例を使用し、ASP.NET Core 3.1 以降の Blazor 承認シナリオに焦点を当てます。 ASP.NET Coreのすべてのリリースに適用される Razor ページと MVC ガイダンスについては、次のリソースを参照してください。

この記事の例では、C# 12 (.NET 8) 以降で使用できる プライマリ コンストラクターを使用します。 詳細については、クラスと構造体のプライマリ コンストラクターの宣言 (C# ドキュメント チュートリアル) とプライマリ コンストラクター (C# ガイド) を参照してください。

サンプル アプリ

この記事の Blazor Web App サンプルは、BlazorWebAppAuthorization サンプル アプリ (dotnet/AspNetCore.Docs.Samples GitHub リポジトリ) (ダウンロード方法) です。 このサンプル アプリでは、事前構成済みのドキュメント オブジェクトを含むシード処理されたアカウントを使用して、この記事の例を示します。 詳細については、サンプルの README ファイル (README.md) を参照してください。

Caution

このサンプル アプリでは、メモリ内データベースを使用してユーザー情報を格納しますが、運用環境のシナリオには適していません。 サンプル アプリはデモンストレーションのみを目的としており、運用環境のアプリの開始点として使用しないでください。

強制認可を使う

認可は IAuthorizationService として実装され、アプリの起動時に ASP.NET Core フレームワークによってサービス コレクションに登録されます。 このサービスは、Razorを使用して、 コンポーネントやその他のクラスで使用できるようになります。

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService には、2 つの AuthorizeAsync メソッド オーバーロードがあります。 オーバーロードの 1 つは、リソースとポリシー名を受け入れます。

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);

もう 1 つのオーバーロードは、評価するリソースと要件のコレクション (IAuthorizationRequirement) を受け入れます。

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);

次の例では、「 リソースベースのハンドラーの作成 」セクションで完全に説明されています。セキュリティで保護されたリソースは、カスタム Document オブジェクトに読み込まれます。 AuthorizeAsyncオーバーロードが呼び出され、現在のユーザーが "SameAuthorPolicy" 承認ポリシーに基づいてドキュメントへのアクセスを許可されているかどうかを判断します。 authorizationResult.Succeededtrueされている場合、ユーザーはドキュメントを作成したためにドキュメントを承認されます (Document.AuthorユーザーのNameと一致します)。

protected override async Task OnParametersSetAsync()
{
    var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

    if (user.Identity is not null && user.Identity.IsAuthenticated)
    {
        var document = DocumentRepository.Find(DocumentId);

        ...

        var authorizationResult = await AuthorizationService
            .AuthorizeAsync(user, document, "SameAuthorPolicy");

        ...
    }
}

リソース ベースのハンドラーを作成する

リソース ベースの承認ハンドラーの作成は、 プレーンな要件ハンドラーの作成と似ています。 カスタム要件クラスを作成し、要件ハンドラー クラスを実装します。 要件クラスの作成の詳細については、「 ポリシーベースの承認: 要件」を参照してください。

次のデモ Document クラスを使用します。

namespace BlazorWebAppAuthorization.Models;

public class Document
{
    public string? Author { get; set; }

    public byte[]? Content { get; set; }

    public Guid ID { get; set; }

    public string? Title { get; set; }
}

ハンドラー クラスは、要件とリソースの種類を指定します。 次の例では、 SameAuthorRequirement 要件と Document リソースを利用するハンドラーを示します。

Services/DocumentAuthorizationHandler.cs:

using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Services;

public class DocumentAuthorizationHandler :
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        SameAuthorRequirement requirement, 
        Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

Program.cs で要件とハンドラーを登録します。

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

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

Startup.ConfigureServices で要件とハンドラーを登録します。

services.AddAuthorization(options =>
{
    options.AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

承認ポリシーの作成の詳細については、「ASP.NET Core での Policy ベースの承認」を参照してください。

次の AccessDocument コンポーネントは、 AuthorizeAsync オーバーロードを呼び出して、現在のユーザーが "SameAuthorPolicy" 承認ポリシーに基づいてドキュメントを表示できるかどうかを判断します。 authorizationResult.Succeededtrueされている場合、ユーザーはドキュメントを作成したためにドキュメントの承認を受けます (Document.AuthorユーザーのNameと一致します)。

Pages/AccessDocument.razor:

@page "/access-document/{documentId}"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppAuthorization.Data
@inject AuthenticationStateProvider AuthStateProvider
@inject IAuthorizationService AuthorizationService
@inject IDocumentRepository DocumentRepository

<h1>Access Document</h1>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p>@message</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized to access this page.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string? message;

    [Parameter]
    public string? DocumentId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var document = DocumentRepository.Find(DocumentId);

            if (document == null)
            {
                message = "Document not found.";
                return;
            }

            var authorizationResult = await AuthorizationService
                .AuthorizeAsync(user, document, "SameAuthorPolicy");

            message = authorizationResult.Succeeded
                ? $"You are authorized for document {DocumentId}."
                : $"You are NOT authorized for document {DocumentId}.";
        }
    }
}

サンプル アプリでは、アプリの各ユーザーが、作成したシードされたドキュメントへのアクセスが承認されます。

運用上の要件

CRUD (作成、読み取り、更新、削除) 操作の結果に基づいて決定を行うには、 OperationAuthorizationRequirement ヘルパー クラスを使用します。 ヘルパー クラスを使用すると、操作の種類ごとに個別のクラスではなく、1 つのハンドラーを記述できます。 次の Operations クラスは、4 つの CRUD 操作の種類をすべて確立します。

using Microsoft.AspNetCore.Authorization.Infrastructure;

public static class Operations
{
    public static readonly OperationAuthorizationRequirement Create =
        new() { Name = nameof(Create) };
    public static readonly OperationAuthorizationRequirement Delete =
        new() { Name = nameof(Delete) };
    public static readonly OperationAuthorizationRequirement Read =
        new() { Name = nameof(Read) };
    public static readonly OperationAuthorizationRequirement Update =
        new() { Name = nameof(Update) };
}

次の DocumentAuthorizationCrudHandler 承認ハンドラーは、リソース、場合によってはユーザーの ID (ロール)、要件の Name プロパティを使用して操作を検証します。

  • すべてのユーザーがドキュメントを読み取ることができます。
  • ドキュメントを作成および更新できるのは、 Admin ロールのユーザーだけです。
  • ドキュメントを削除できるのは、 SuperUser ロールのユーザーだけです。

Services/DocumentAuthorizationCrudHandler.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Services;

public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement, 
        Document resource)
    {
        if (requirement.Name == Operations.Create.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Delete.Name &&
            context.User.IsInRole("SuperUser"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Update.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

サービスがアプリに登録されている場所:

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

操作で AuthorizeAsync のオーバーロードを呼び出して、承認結果を返します。

ドキュメントを 作成 するための承認の場合:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Create);

ドキュメントを 読み取 るための承認の場合:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Read);

ドキュメントを 削除 する権限の場合:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Delete);

ドキュメントを 更新 するための承認の場合:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Update);

サンプル アプリAccessDocumentCrud ページで、次の手順を実行します。

  • Leela (leela@contoso.com) は、 Admin および SuperUserとして、リソースに対して完全な CRUD 操作を実行できます。
  • Adminであるハリー (harry@contoso.com) だけが、リソースの作成、読み取り、更新を行うことができます。
  • Sarah (sarah@contoso.com) は、単なる SuperUser であるため、リソースを削除および読み取りできます。