Edit

Resource-based authorization in ASP.NET Core MVC

This article describes how to authorize users for access to app resources.

In an app, a resource is typically represented by a C# class that includes data stored in a collection, such as a byte[] array. The class usually contains additional metadata pertaining to the resource, such as a unique resource identifier, dates, authors, source information, and a friendly name for display in a UI. The collection that holds resource data is usually loaded from physical file content, a cloud storage object, an in-memory object, or data from a database.

Resource-based authorization requires special attention in ASP.NET Core apps. Attribute evaluation occurs before data binding and before execution of an action that loads a resource. Declarative authorization with an [Authorize] attribute doesn't suffice for resource-based authorization. Instead, the app must invoke a custom authorization method—an approach known as imperative authorization.

View or download sample code (how to download).

Create an ASP.NET Core app with user data protected by authorization contains a sample app that uses resource-based authorization.

Examples in this article use primary constructors, available in C# 12 (.NET 8) or later. For more information, see Declare primary constructors for classes and structs (C# documentation tutorial) and Primary constructors (C# Guide). Sample apps that accompany the article that target versions of .NET earlier than .NET 8 use constructor injection.

Use imperative authorization

Authorization is implemented as an IAuthorizationService, which is registered in the service collection at app startup by the ASP.NET Core framework. The service is made available to classes and actions via dependency injection. The following controller also injects a document repository, which the developer creates and registers in the service container to manage document operations:

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 has two AuthorizeAsync method overloads. One of the overloads accepts a resource and policy name:

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

The other overload accepts a resource and collection of requirements (IAuthorizationRequirement) to evaluate:

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

In the following example, the secured resource is loaded into a custom Document object. An AuthorizeAsync overload is invoked to determine whether the current user is allowed to edit the document via a custom "EditPolicy" authorization policy. If authorizationResult.Succeeded is true, the user is authorized for the document because they authored the document (Document.Author matches the user's Name).

Note

The following example assumes successful authentication with the User property set.

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

    ...

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

    ...
}

Create a resource-based handler

Creating a resource-based authorization handler is similar to creating a plain requirements handler. Create a custom requirement class and implement a requirement handler class. For more information on creating a requirement class, see the Policy-based authorization: Requirements.

The handler class specifies the requirement and resource type. The following example demonstrates a handler utilizing a SameAuthorRequirement requirement and a Document resource:

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

In the preceding example, imagine that SameAuthorRequirement is a special case of a more generic SpecificAuthorRequirement class. The SpecificAuthorRequirement class (not shown) contains a Name property representing the name of the author. The Name property could be set to the current user.

Register the requirement and handler in Program.cs:

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

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

Register the requirement and handler in Startup.ConfigureServices:

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

For more information on creating authorization policies, see Policy-based authorization in ASP.NET Core.

Operational requirements

To make decisions based on the outcomes of CRUD (Create, Read, Update, Delete) operations, use the OperationAuthorizationRequirement helper class. This class enables you to write a single handler instead of an individual class for each operation type. To use it, provide some operation names:

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) };
}

The handler is implemented as follows, using an OperationAuthorizationRequirement requirement and a Document resource:

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

The preceding handler validates the operation using the resource, the user's identity, and the requirement's Name property.

Challenge and forbid with an operational resource handler

This section shows how the challenge and forbid action results are processed and how challenge and forbid differ.

When authorization fails but the user is authenticated, the app can return a ForbidResult, which informs authentication middleware that authorization failed. Return a ChallengeResult for unauthenticated users. For interactive browser clients, it may be appropriate to redirect the user to a login page.

Note

The following example assumes successful authentication with the User property set.

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();
}