Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access Route Data / Value Provider data in a service in ASP.NET Core?

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?

like image 227
NightOwl888 Avatar asked Feb 15 '18 17:02

NightOwl888


2 Answers

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.

like image 67
NightOwl888 Avatar answered Oct 17 '22 14:10

NightOwl888


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

like image 4
Marc Annous Avatar answered Oct 17 '22 13:10

Marc Annous