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

内部的には、ロールベースの認可クレームベースの認可によって、要件、要件ハンドラー、および事前構成済みポリシーが使われています。 これらの構成要素では、コードでの認可評価の式がサポートされています。 その結果、多機能で再利用できるテスト可能な認可構造が作成されます。

認可ポリシーは、1 つまたは複数の要件で構成されます。 それを、アプリの Program.cs ファイルで、認可サービス構成の一部として登録します。

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

前の例では、"AtLeast21" ポリシーが作成されます。 これには最低年齢に関する 1 つの要件があり、パラメーターとして要件に提供されます。

IAuthorizationService

認可が成功したかどうかを決定するプライマリ サービスは IAuthorizationService です。

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

上のコードでは、IAuthorizationService の 2 つのメソッドが強調されています。

IAuthorizationRequirement は、メソッドを持たないマーカー サービスであり、認可が成功したかどうかを追跡するためのメカニズムです。

IAuthorizationHandler によって、要件が満たされているかどうかがチェックされます。

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext クラスは、要件が満たされているかどうかをマークするためにハンドラーによって使用されます。

 context.Succeed(requirement)

次のコードは、認可サービスの既定の実装を簡単に (コメントで注釈を付けて) 示したものです。

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

次に示すコードは、一般的な認可サービスの構成です。

// Add all of your handlers to DI.
builder.Services.AddSingleton<IAuthorizationHandler, MyHandler1>();
// MyHandler2, ...

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

// Configure your policies
builder.Services.AddAuthorization(options =>
      options.AddPolicy("Something",
      policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));

認可には、IAuthorizationService[Authorize(Policy = "Something")]、または RequireAuthorization("Something") を使います。

MVC コントローラーにポリシーを適用する

Razor Pages を使うアプリについては、「Razor Pages にポリシーを適用する」セクションをご覧ください。

コントローラーにポリシーを適用するには、[Authorize] 属性を使用してポリシー名を指定します:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

コントローラー レベルとアクション レベルで複数のポリシーが適用される場合、アクセスが許可されるにはすべてのポリシーを満たしている必要があります:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

Razor Pages にポリシーを適用する

Razor Pages にポリシーを適用するには、[Authorize] 属性を使用してポリシー名を指定します。 次に例を示します。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthorizationPoliciesSample.Pages;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Model : PageModel { }

Razor Page ハンドラー レベルでポリシーを適用することは "できません"。Page に適用する必要があります。

認可規則を使用することで、Razor Pages にポリシーを適用することもできます。

エンドポイントにポリシーを適用する

エンドポイントにポリシーを適用するには、RequireAuthorization を使用してポリシー名を指定します。 次に例を示します。

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

要件

認可要件は、ポリシーで現在のユーザー プリンシパルを評価するために使用できるデータ パラメーターのコレクションです。 "AtLeast21" ポリシーの要件は、1 つのパラメーターつまり最低年齢です。 要件は、空のマーカー インターフェイスである IAuthorizationRequirement を実装します。 パラメーター化された最低年齢要件は、次のように実装できます。

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

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

    public int MinimumAge { get; }
}

認可ポリシーに複数の認可要件が含まれている場合、ポリシーの評価が成功するには、すべての要件が合格する必要があります。 つまり、1 つの認可ポリシーに追加された複数の認可要件は、AND ベースで処理されます。

Note

要件には、データまたはプロパティがなくてもかまいません。

認可ハンドラー

認可ハンドラーでは、要件のプロパティの評価が行われます。 認可ハンドラーにより、指定された AuthorizationHandlerContext に対して要件が評価され、アクセスが許可されるかどうかが判断されます。

1 つの要件に複数のハンドラーを指定できます。 ハンドラーは AuthorizationHandler<TRequirement> を継承できます。ここで TRequirement は処理される要件です。 または、ハンドラーで IAuthorizationHandler を直接実装して、複数の種類の要件を処理することもできます。

1 つの要件にハンドラーを使用する

次の例では、最低年齢ハンドラーで 1 つの要件を処理する 1 対 1 の関係が示されています。

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com");

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

上記のコードにより、現在のユーザー プリンシパルに、既知の信頼された発行者によって発行された生年月日クレームがあるかどうかが判断されます。 クレームが見つからないと認可は行われず、その場合は完了したタスクが返されます。 クレームが存在する場合は、ユーザーの年齢が計算されます。 要件で定義されている最低年齢をユーザーが満たしている場合、認可は成功と見なされます。 認可が成功すると、満たされた要件を唯一のパラメーターとして context.Succeed が呼び出されます。

複数の要件にハンドラーを使用する

次の例は、アクセス許可ハンドラーで 3 種類の異なる要件を処理できる一対多の関係を示したものです。

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource)
                    || IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission || requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}

上のコードでは、成功とマークされていない要件が含まれる PendingRequirements プロパティが走査されます。 ReadPermission 要件の場合、要求されたリソースにアクセスするには、ユーザーが所有者またはスポンサーである必要があります。 EditPermission または DeletePermission 要件の場合は、要求されたリソースにアクセスするには所有者である必要があります。

ハンドラーの登録

構成の間にサービス コレクションでハンドラーを登録します。 次に例を示します。

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

上のコードでは、MinimumAgeHandler がシングルトンとして登録されます。 ハンドラーは、組み込みのサービス有効期間のいずれかを使って登録できます。

IAuthorizationRequirementIAuthorizationHandler の両方を実装する 1 つのクラスに、要件とハンドラーの両方をバンドルすることができます。 これにより、ハンドラーと要件の間に緊密な結合が作成されるので、単純な要件とハンドラーにのみ推奨されます。 両方のインターフェイスが実装されているクラスを作成すると、要件を独自に処理できる組み込みの PassThroughAuthorizationHandler があるため、ハンドラーを DI で登録する必要がなくなります。

AssertionRequirement が完全な自己完結型クラスの要件とハンドラーの両方である良い例については、「AssertionRequirement クラス」をご覧ください。

ハンドラーから返す必要があるもの

ハンドラーの例Handle メソッドからは値が返されていないことに注意してください。 成功または失敗の状態はどのようにして示されるのでしょうか。

  • ハンドラーは、context.Succeed(IAuthorizationRequirement requirement) を呼び出し、検証が成功した要件を渡すことによって、成功を示します。

  • 同じ要件が他のハンドラーで成功する可能性があるので、一般に、ハンドラーで失敗を処理する必要はありません。

  • 他の要件ハンドラーが成功した場合でも、失敗を保証するには、context.Fail を呼び出します。

あるハンドラーで context.Succeed または context.Fail が呼び出された場合でも、他のすべてのハンドラーが呼び出されます。 これにより、別のハンドラーで検証が成功したり要件が失敗した場合でも発生するログ記録などの副作用を、要件で生成できます。 false に設定すると、InvokeHandlersAfterFailure プロパティにより、context.Fail が呼び出されたときのハンドラーの実行が省略されます。 InvokeHandlersAfterFailure の既定値は true で、すべてのハンドラーが呼び出されます。

Note

認可ハンドラーは、認証が失敗した場合でも呼び出されます。 また、ハンドラーは任意の順序で実行できるため、特定の順序で呼び出されることに依存させないでください。

要件に対して複数のハンドラーが必要な理由

評価を OR ベースで行いたい場合は、1 つの要件に対して複数のハンドラーを実装します。 たとえば、Microsoft にはキー カードでのみ開くドアがあります。 キー カードを家に忘れてきた場合は、受付で一時的なステッカーを印刷し、ドアを開けてもらいます。 このシナリオでは、要件は BuildingEntry の 1 つですが、それぞれが 1 つの要件を調べる複数のハンドラーを使用します。

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }

BadgeEntryHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "BadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "TemporaryBadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

両方のハンドラーを登録する必要があります。 ポリシーで BuildingEntryRequirement が評価されるときに、いずれかのハンドラーが成功した場合、ポリシーの評価は成功します。

func を使用してポリシーを満たす

コードで表現してポリシーを簡単に満たすことができる場合があります。 RequireAssertion ポリシー ビルダーでポリシーを構成するときに、Func<AuthorizationHandlerContext, bool> を指定できます。

たとえば、前の BadgeEntryHandler を次のように書き換えることができます。

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context => context.User.HasClaim(c =>
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://microsoftsecurity")));
});

ハンドラーで MVC 要求コンテキストにアクセスする

HandleRequirementAsync メソッドには 2 つのパラメーターがあります。AuthorizationHandlerContext と、処理対象の TRequirement です。 MVC や SignalR などのフレームワークにより、AuthorizationHandlerContextResource プロパティに任意のオブジェクトが自由に追加されて、追加の情報が渡されます。

エンドポイント ルーティングを使用している場合、通常、認可は認可ミドルウェアによって処理されます。 この場合、Resource プロパティは HttpContext のインスタンスです。 コンテキストを使用して現在のエンドポイントにアクセスでき、それを使用してルーティング先の基になるリソースをプローブできます。 次に例を示します。

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

従来のルーティングでは、または MVC の認可フィルターの一部として認可が行われるときは、Resource の値は AuthorizationFilterContext のインスタンスです。 このプロパティを使用して、HttpContextRouteData、および MVC と Razor Pages によって提供される他のすべてのものにアクセスできます。

Resource プロパティの使用はフレームワーク固有です。 Resource プロパティの情報を使用すると、認可ポリシーが特定のフレームワークに制限されます。 is キーワードを使って Resource プロパティをキャストしてから、キャストが成功したことを確認して、他のフレームワークで実行したときにコードが InvalidCastException でクラッシュしないようにする必要があります。

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

すべてのユーザーの認証をグローバルに要求する

すべてのユーザーの認証をグローバルに要求する方法の詳細については、「認証されたユーザーを要求する」を参照してください。

外部サービスを使用した認可のサンプル

AspNetCore.Docs.Samples のサンプル コードは、外部認可サービスによって追加の認可要件を実装する方法を示しています。 サンプルの Contoso.API プロジェクトは、Azure AD によってセキュリティで保護されています。 Contoso.Security.API プロジェクトからの追加の認可チェックでは、Contoso.API クライアント アプリが GetWeather API を呼び出すことができるかどうかを示すペイロードが返されます。

サンプルの構成

  1. Azure Active Directory (Azure AD) テナントアプリケーション登録を作成します。
  • AppRole を割り当てます。
  • [API のアクセス許可] で、AppRole をアクセス許可として追加し、管理者の同意を付与します。 このセットアップでは、このアプリ登録は API と API を呼び出すクライアントの両方を表すことに注意してください。 必要に応じて、2 つのアプリ登録を作成できます。 このセットアップを使用している場合は、必ず API のアクセス許可のみを実行し、クライアントのみのアクセス許可ステップとして AppRole を追加してください。 クライアント アプリの登録でのみ、クライアント シークレットを生成する必要があります。
  1. 次の設定で Contoso.API プロジェクトを構成します。
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<Tenant name from AAD properties>.onmicrosoft.com">,
    "TenantId": "<Tenant Id from AAD properties>",
    "ClientId": "<Client Id from App Registration representing the API>"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
  1. 次の設定で Contoso.Security.API を構成します。
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. ContosoAPI.postman_collection.json ファイルを Postman にインポートし、次を使用して環境を構成します。

    • ClientId: API を呼び出すクライアントを表すアプリ登録からのクライアント ID。
    • clientSecret: API を呼び出すクライアントを表すアプリ登録からのクライアント シークレット。
    • TenantId: AAD プロパティのテナント ID
  2. ソリューションを実行し、Postman を使用して API を呼び出します。 Contoso.Security.API.SecurityPolicyController にブレークポイントを追加し、Get Weather が許可されているかどうかをアサートするために使用されるクライアント ID が渡されていることを確認できます。

その他のリソース

内部的には、ロールベースの認可クレームベースの認可によって、要件、要件ハンドラー、および事前構成済みポリシーが使われています。 これらの構成要素では、コードでの認可評価の式がサポートされています。 その結果、多機能で再利用できるテスト可能な認可構造が作成されます。

認可ポリシーは、1 つまたは複数の要件で構成されます。 それは、Startup.ConfigureServices メソッドで認可サービス構成の一部として登録されます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

前の例では、"AtLeast21" ポリシーが作成されます。 これには最低年齢に関する 1 つの要件があり、パラメーターとして要件に提供されます。

IAuthorizationService

認可が成功したかどうかを決定するプライマリ サービスは IAuthorizationService です。

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

上のコードでは、IAuthorizationService の 2 つのメソッドが強調されています。

IAuthorizationRequirement は、メソッドを持たないマーカー サービスであり、認可が成功したかどうかを追跡するためのメカニズムです。

IAuthorizationHandler によって、要件が満たされているかどうかがチェックされます。

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext クラスは、要件が満たされているかどうかをマークするためにハンドラーによって使用されます。

 context.Succeed(requirement)

次のコードは、認可サービスの既定の実装を簡単に (コメントで注釈を付けて) 示したものです。

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

次のコードは、一般的な ConfigureServices を示したものです。

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddControllersWithViews();
    services.AddRazorPages();
}

認可には、IAuthorizationService または [Authorize(Policy = "Something")] を使います。

MVC コントローラーにポリシーを適用する

Razor Pages を使っている場合は、このドキュメントの「Razor Pages にポリシーを適用する」をご覧ください。

ポリシーは、ポリシー名と共に [Authorize] 属性を使用することでコントローラーに適用されます。 次に例を示します。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

Razor Pages にポリシーを適用する

Razor Pages にポリシーを適用するには、[Authorize] 属性を使用してポリシー名を指定します。 次に例を示します。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

Razor Page ハンドラー レベルでポリシーを適用することは "できません"。Page に適用する必要があります。

認可規則を使用することで、Razor Pages にポリシーを適用できます。

要件

認可要件は、ポリシーで現在のユーザー プリンシパルを評価するために使用できるデータ パラメーターのコレクションです。 "AtLeast21" ポリシーの要件は、1 つのパラメーターつまり最低年齢です。 要件は、空のマーカー インターフェイスである IAuthorizationRequirement を実装します。 パラメーター化された最低年齢要件は、次のように実装できます。

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

認可ポリシーに複数の認可要件が含まれている場合、ポリシーの評価が成功するには、すべての要件が合格する必要があります。 つまり、1 つの認可ポリシーに追加された複数の認可要件は、AND ベースで処理されます。

Note

要件には、データまたはプロパティがなくてもかまいません。

認可ハンドラー

認可ハンドラーでは、要件のプロパティの評価が行われます。 認可ハンドラーにより、指定された AuthorizationHandlerContext に対して要件が評価され、アクセスが許可されるかどうかが判断されます。

1 つの要件に複数のハンドラーを指定できます。 ハンドラーは AuthorizationHandler<TRequirement> を継承できます。ここで TRequirement は処理される要件です。 または、ハンドラーで IAuthorizationHandler を実装して、複数の種類の要件を処理することもできます。

1 つの要件にハンドラーを使用する

次の例では、最低年齢ハンドラーで 1 つの要件を利用する 1 対 1 の関係が示されています。

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

上記のコードにより、現在のユーザー プリンシパルに、既知の信頼された発行者によって発行された生年月日クレームがあるかどうかが判断されます。 クレームが見つからないと認可は行われず、その場合は完了したタスクが返されます。 クレームが存在する場合は、ユーザーの年齢が計算されます。 要件で定義されている最低年齢をユーザーが満たしている場合、認可は成功と見なされます。 認可が成功すると、満たされた要件を唯一のパラメーターとして context.Succeed が呼び出されます。

複数の要件にハンドラーを使用する

次の例は、アクセス許可ハンドラーで 3 種類の異なる要件を処理できる一対多の関係を示したものです。

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

上のコードでは、成功とマークされていない要件が含まれる PendingRequirements プロパティが走査されます。 ReadPermission 要件の場合、要求されたリソースにアクセスするには、ユーザーが所有者またはスポンサーである必要があります。 EditPermission または DeletePermission 要件の場合は、要求されたリソースにアクセスするユーザーは所有者である必要があります。

ハンドラーの登録

ハンドラーは、構成の間にサービス コレクションで登録されます。 次に例を示します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

上のコードでは、services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); を呼び出すことによって、MinimumAgeHandler がシングルトンとして登録されます。 ハンドラーは、組み込みのサービス有効期間のいずれかを使って登録できます。

IAuthorizationRequirementIAuthorizationHandler の両方を実装する 1 つのクラスに、要件とハンドラーの両方をバンドルすることができます。 これにより、ハンドラーと要件の間に緊密な結合が作成されるので、単純な要件とハンドラーにのみ推奨されます。 両方のインターフェイスが実装されているクラスを作成すると、要件を独自に処理できる組み込みの PassThroughAuthorizationHandler があるため、ハンドラーを DI で登録する必要がなくなります。

AssertionRequirement が完全な自己完結型クラスの要件とハンドラーの両方である良い例については、「AssertionRequirement クラス」をご覧ください。

ハンドラーから返す必要があるもの

ハンドラーの例Handle メソッドからは値が返されていないことに注意してください。 成功または失敗の状態はどのようにして示されるのでしょうか。

  • ハンドラーは、context.Succeed(IAuthorizationRequirement requirement) を呼び出し、検証が成功した要件を渡すことによって、成功を示します。

  • 同じ要件が他のハンドラーで成功する可能性があるので、一般に、ハンドラーで失敗を処理する必要はありません。

  • 他の要件ハンドラーが成功した場合でも、失敗を保証するには、context.Fail を呼び出します。

あるハンドラーで context.Succeed または context.Fail が呼び出された場合でも、他のすべてのハンドラーが呼び出されます。 これにより、別のハンドラーで検証が成功したり要件が失敗した場合でも発生するログ記録などの副作用を、要件で生成できます。 false に設定すると、InvokeHandlersAfterFailure プロパティにより、context.Fail が呼び出されたときのハンドラーの実行が省略されます。 InvokeHandlersAfterFailure の既定値は true で、すべてのハンドラーが呼び出されます。

Note

認可ハンドラーは、認証が失敗した場合でも呼び出されます。

要件に対して複数のハンドラーが必要な理由

評価を OR ベースで行いたい場合は、1 つの要件に対して複数のハンドラーを実装します。 たとえば、Microsoft にはキー カードでのみ開くドアがあります。 キー カードを家に忘れてきた場合は、受付で一時的なステッカーを印刷し、ドアを開けてもらいます。 このシナリオでは、要件は BuildingEntry の 1 つですが、それぞれが 1 つの要件を調べる複数のハンドラーを使用します。

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

両方のハンドラーを登録する必要があります。 ポリシーで BuildingEntryRequirement が評価されるときに、いずれかのハンドラーが成功した場合、ポリシーの評価は成功します。

func を使用してポリシーを満たす

コードで表現してポリシーを簡単に満たすことができる場合があります。 RequireAssertion ポリシー ビルダーでポリシーを構成するときに、Func<AuthorizationHandlerContext, bool> を指定できます。

たとえば、前の BadgeEntryHandler を次のように書き換えることができます。

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

ハンドラーで MVC 要求コンテキストにアクセスする

認可ハンドラーで実装する HandleRequirementAsync メソッドには、AuthorizationHandlerContext と処理対象の TRequirement の 2 つのパラメーターがあります。 MVC や SignalR などのフレームワークにより、AuthorizationHandlerContextResource プロパティに任意のオブジェクトが自由に追加されて、追加の情報が渡されます。

エンドポイント ルーティングを使用している場合、通常、認可は認可ミドルウェアによって処理されます。 この場合、Resource プロパティは HttpContext のインスタンスです。 コンテキストを使用して現在のエンドポイントにアクセスでき、それを使用してルーティング先の基になるリソースをプローブできます。 次に例を示します。

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

従来のルーティングでは、または MVC の認可フィルターの一部として認可が行われるときは、Resource の値は AuthorizationFilterContext のインスタンスです。 このプロパティを使用して、HttpContextRouteData、および MVC と Razor Pages によって提供される他のすべてのものにアクセスできます。

Resource プロパティの使用はフレームワーク固有です。 Resource プロパティの情報を使用すると、認可ポリシーが特定のフレームワークに制限されます。 is キーワードを使って Resource プロパティをキャストしてから、キャストが成功したことを確認して、他のフレームワークで実行したときにコードが InvalidCastException でクラッシュしないようにする必要があります。

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

すべてのユーザーの認証をグローバルに要求する

すべてのユーザーの認証をグローバルに要求する方法の詳細については、「認証されたユーザーを要求する」を参照してください。