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

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

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

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

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

承認によって保護されたユーザー データでの ASP.NET Core アプリの作成」には、リソース ベースの認可を使ったサンプル アプリが含まれています。

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

強制認可を使う

認可は IAuthorizationService として実装され、アプリの起動時に ASP.NET Core フレームワークによってサービス コレクションに登録されます。 このサービスは、 依存関係の挿入を介してクラスとアクションで使用できるようになります。 次のコントローラーは、ドキュメント リポジトリも挿入します。このリポジトリは、開発者が作成し、ドキュメント操作を管理するためにサービス コンテナーに登録します。

public class DocumentController(IAuthorizationService authorizationService,
    IDocumentRepository documentRepository) : Controller
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _documentRepository;

    public DocumentController(IAuthorizationService authorizationService,
        IDocumentRepository documentRepository)
    {
        _authorizationService = authorizationService;
        _documentRepository = documentRepository;
    }

    ...
}

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オーバーロードが呼び出され、現在のユーザーがカスタムの "EditPolicy" 承認ポリシーを使用してドキュメントを編集できるかどうかを判断します。 authorizationResult.Succeededtrueされている場合、ユーザーはドキュメントを作成したためにドキュメントの承認を受けます (Document.AuthorユーザーのNameと一致します)。

Note

次の例では、 User プロパティが設定された認証が成功していることを前提としています。

[HttpGet]
public async Task<IActionResult> Edit(Guid documentId)
{
    Document document = _documentRepository.Find(documentId);

    ...

    var authorizationResult = await _authorizationService
        .AuthorizeAsync(User, document, "EditPolicy");

    ...
}

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

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

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

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 { }

前の例では、SameAuthorRequirement がより汎用的な SpecificAuthorRequirement クラスの特殊なケースであると仮定します。 SpecificAuthorRequirement クラス (ここでは示されていません) には、作成者の名前を表す Name プロパティが含まれています。 Name プロパティは、現在のユーザーに設定できます。

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

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

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

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

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

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

運用上の要件

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

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

ハンドラーは、OperationAuthorizationRequirement 要件と Document リソースを使って次のように実装されます。

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

        return Task.CompletedTask;
    }
}

上記のハンドラーは、リソース、ユーザーの ID、要件の Name プロパティを使用して操作を検証します。

運用リソース ハンドラーによる異議申し立てと禁止

このセクションでは、チャレンジと禁止アクションの結果がどのように処理されるかについて、およびチャレンジと禁止の違いについて説明します。

承認が失敗してもユーザーが認証されると、アプリは ForbidResultを返すことができます。これは、承認が失敗したことを認証ミドルウェアに通知します。 未認証ユーザーに対して ChallengeResult を返します。 対話型のブラウザー クライアントの場合は、ユーザーをログイン ページにリダイレクトすることが適切な場合があります。

Note

次の例では、 User プロパティが設定された認証が成功していることを前提としています。

if ((await _authorizationService
    .AuthorizeAsync(User, document, Operations.Read)).Succeeded)
{
    return View(document);
}
else if (User.Identity?.IsAuthenticated ?? false)
{
    return new ForbidResult();
}
else
{
    return new ChallengeResult();
}