13

I am attempting to write a Policy-based Authorization Handler. The business logic of the handler needs to use the record id of the current request that is passed in through the default route.

[Authorize(Roles = "TaskAdmin", Policy = "RecordOwner")]
public IActionResult Index(int id) // <-- Need this id
{
    // <snip>

    return View();
}

Policy

Here is the class where I need to access the id route value.

public class RecordOwnerHandler : AuthorizationHandler<RecordOwnerRequirement>
{
    private readonly ApplicationDbContext dbContext;

    public RecordOwnerHandler(ApplicationDbContext dbContext)
    {
        this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordOwnerRequirement requirement)
    {
        if (IsUserAuthorized(context))
        {
            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 IsUserAuthorized(AuthorizationHandlerContext context)
    {
        //****************************************
        // Need the id here...
        //****************************************

        // Return the result
        return true;
    }
}

Startup

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    // *** Add policy for record owner ***
    services.AddAuthorization(options =>
    {
        options.AddPolicy("RecordOwner", policy =>
            policy.Requirements.Add(new RecordOwnerRequirement()));
    });

    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();

    // *** Register record owner handler with the DI container ***
    services.AddTransient<IAuthorizationHandler, RecordOwnerHandler>(); 

    services.AddMvc();
}

What I Tried

  1. I tried using the IHttpContextAccessor as a constructor parameter of RecordOwnerHandler, but IHttpContextAccessor.HttpContext doesn't seem to contain the RouteData of the request.

  2. I did several Google searches to see if there was any info about how to do this and came up blank.

  3. Then I dug through the source code for both Routing and Model Binding, but can't seem to find an abstraction that is meant for injecting route values into services.

I realize I could try to parse this info out of the URL, but I am hoping for a cleaner way to get the value.

So, how can I access route values and/or value provider data inside of a service in ASP.NET Core 2.0?

RickAndMSFT
  • 20,912
  • 8
  • 60
  • 78
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Does [this](https://github.com/aspnet/Mvc/blob/41efa409a4188926059001e8b2216d3376b0b705/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs#L56) help? – Mike Feb 15 '18 at 17:42
  • @Mike - Nope. However, I was able to work it out by searching the repo for "accessor" to see if there was one that suited my needs. – NightOwl888 Feb 15 '18 at 18:31

4 Answers4

21

Route values can be accessed by using the ActionContextAccessor class.

DI Registration

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

Usage

public class RecordOwnerHandler : AuthorizationHandler<RecordOwnerRequirement>
{
    private readonly ApplicationDbContext dbContext;
    private readonly IActionContextAccessor actionContextAccessor;

    public RecordOwnerHandler(ApplicationDbContext dbContext, IActionContextAccessor actionContextAccessor)
    {
        this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        this.actionContextAccessor = actionContextAccessor ?? throw new ArgumentNullException(nameof(actionContextAccessor));
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordOwnerRequirement requirement)
    {
        if (IsUserAuthorized(context))
        {
            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 IsUserAuthorized(AuthorizationHandlerContext context)
    {
        // Now the id route value can be accessed directly...
        var id = this.actionContextAccessor.ActionContext.RouteData.Values["id"];

        // Use the dbContext to compare the id against the database...

        // Return the result
        return true;
    }
}

NOTE: I would still like to find out a way to access the value providers to do this, so it wouldn't matter if the parameter is passed through route values, query string, form values, etc.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • From my understanding of the RouteData values. They are aggregated from the values into the RouteValueDictionary. So I am a little confused about the footnote in your answer. – Nkosi Feb 16 '18 at 03:20
  • In MVC 5, the values that are provided to the `ModelBinder` and to the action method parameters are read through a collection of **[Value Providers](https://stackoverflow.com/a/36606015)** that read the data from query string, route values, form values and various other places in the context. I can see by analyzing the source that it is still done in much the same way in ASP.NET Core, however I haven't found a way to get access to them to read the values they provide. – NightOwl888 Feb 16 '18 at 08:17
  • Now I understand what you mean. Thank you for clarifying. – Nkosi Feb 16 '18 at 09:41
  • 1
    I believe you can and should register it as a singleton like `services.AddTransient();` See https://github.com/aspnet/Mvc/blob/master/test/WebSites/VersioningWebSite/Startup.cs for an example. – Jeremy Cook Sep 04 '18 at 19:34
  • This does nothing in .NET 5.0. – Phill Sep 09 '21 at 18:55
6

For future reference, starting .NET Core 5.0, the HttpContext is now passed instead, so you can do:

if (context.Resource is HttpContext httpContext)
{
   var value = httpContext.GetRouteValue("key");
}

See this for reference [AspNetCore] Authorization resource in endpoint routing will now be HttpContext

Marc Annous
  • 417
  • 5
  • 13
1

Don't know about .NET Core 2.0, but in 3.0 you can access route values with the help of IHttpContextAccessor, with a service implementation that will be provided automatically for you by the framework (so no need to register anything).

public class RecordOwnerHandler : AuthorizationHandler<RecordOwnerRequirement>
{
    private readonly ApplicationDbContext dbContext;
    private readonly IHttpContextAccessor httpContextAccessor;

    public RecordOwnerHandler(ApplicationDbContext dbContext, IHttpContextAccessor httpContextAccessor)
    {
        this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordOwnerRequirement requirement)
    {
        if (IsUserAuthorized(context))
        {
            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 IsUserAuthorized(AuthorizationHandlerContext context)
    {
        if (!_httpContextAccessor.HttpContext.Request.RouteValues.TryGetValue("id", out var id))
        {
            return false;
        }

        // TODO Whatever logic you need to perform with the id

        return true;
    }
}
Marcus W
  • 589
  • 6
  • 11
1

I have the same use case and tried accessing the route value through the endpoint metadata in the AuthorizationHandler as recommended in the Access MVC request context in handlers section from the ASP.NET Core 5 Security docs:

if (!(context.Resource is Endpoint endpoint)) throw new InvalidOperationException("endpoint routing required");
var id = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>()?.RouteValues["id"];

But the context.Resource was of type DefaultHttpContext, so instead I had to do this:

public class RecordOwnerHandler : AuthorizationHandler<RecordOwnerRequirement>
{
    private readonly ApplicationDbContext dbContext;

    public RecordOwnerHandler(ApplicationDbContext dbContext)
    {
        this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordOwnerRequirement requirement) {
        if (IsUserAuthorized(context))
        {
            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 IsUserAuthorized(AuthorizationHandlerContext context)
    {
        //****************************************
        // Need the id here...
        //****************************************
        if (!(context.Resource is DefaultHttpContext httpContext)) throw new InvalidOperationException("DefaultHttpContext expected");
        var id = httpContext.Request.RouteValues["id"];    

        // Return the result
        return true;
    }
}
Jay Douglass
  • 4,828
  • 2
  • 27
  • 19