Select Page

To allow access to a razor page using a filter based on a route value.

First create an interface:

    public interface IProductLocator
    {
        Task<int?> GetProductIdAsync(PageHandlerExecutingContext context);
    }

Second create a class for the Attribute:

using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyWebSite.Attributes
{
    public class ProductAuthorisationAttribute : Attribute, IFilterMetadata, IProductLocator
    {
        public string Key { get; set; }

        public async Task<int?> GetProductIdAsync(PageHandlerExecutingContext context)
        {
            if (TryGetValue(context.RouteData.Values, out object argument, out string usedKey)
                && int.TryParse(argument.ToString(), out int productId))
            {
                return await Task.FromResult<int?>(productId);
            }

            throw new InvalidOperationException(
                $"Unable to get ProductId from argument: {usedKey}.  Ensure argument exists as integer.");
        }

        private bool TryGetValue(IDictionary<string, object> actionArguments,
            out object argument, out string usedKey)
        {
            usedKey = Key;

            if (string.IsNullOrWhiteSpace(usedKey))
            {
                usedKey = "productId";
            }

            return actionArguments.TryGetValue(usedKey, out argument);
        }
    }
}

Third step, create the Filter class (which calls a repository method productSecurity.CheckPermissionsAsync that returns a boolean value, true if the user has access to the Product, false otherwise. This repository method contains the business logic to decided whether or not the user has access.)

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using MyWebSite.Attributes;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace MyWebSite.Filters
{
    public class ProductAuthorisationFilter : IAsyncPageFilter
    {
        private readonly IProductSecurity productSecurity;

        public ProductAuthorisationFilter(IProductSecurity productSecurity)
        {
            this.productSecurity= productSecurity;
        }

        public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
        {
            var filters = context.Filters;
            var productAuthorisationAttribute = filters
                .OfType<ProductAuthorisationAttribute>()
                .FirstOrDefault(); 

            if (productAuthorisationAttribute != null)
            {
                int? productId = await productAuthorisationAttribute.GetProductIdAsync(context);
                var userName = context.HttpContext.User?.Identity.Name;
                var hasAccess = await productSecurity.CheckPermissionsAsync(userName, productId.Value);
                if (!hasAccess)
                {
                    context.Result = new UnauthorizedResult();
                    throw new UnauthorizedAccessException();
                }
            }

            await next.Invoke();
        }

        public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
        {
            await Task.CompletedTask;
        }
    }
}

Step 4, in your website’s Startup.cs file, add the filter in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    // ... other code omitted

    services.AddRazorPages()
                .AddMvcOptions(options =>
                {
                    options.Filters.Add(typeof(ProductAuthorisationFilter));
                });
            
    // ... other code omitted
}

Finally, the Filter & Attribute combination can be used to protect a page. The razor page has a route parameter (in this case ‘productId’):

@page "/Product/{productId:int}/Details"
@model MyWebSite.Product.DetailsModel
@{
    ViewData["Title"] = "Product Details";
}

// ... remaining razor page code omitted ...

And the [ProductAuthorization] attribute is applied to the razor page code behind. Note also, the standard [Authorize] attribute is applied to prevent anonymous access.

namespace MyWebSite.Pages.Products
{
    [Authorize]
    [ProductAuthorisation]
    public class DetailsModel : PageModel
    {
        // ... remaining code omitted ...
    }
}