11

In my ASP .NET Core 3.1 MVC app, I use endpoint routing like so

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            endpoints.MapControllerRoute(
                name: "access",
                pattern: "access/",
                defaults: new { controller = "Home", action = "Access" });
        });

So browsing to /access, launches the Access action, where the app checks if user complies with some access requirements.

if (access checks...)
{
    return View();
}

Now I would prefer having this check in a custom middleware (or possibly a custom authorize attribute) instead of having it in the Controller. So my question to you is, how should I rewrite the UseEndPoints call, to include a custom middleware for the /access area?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
user2768479
  • 716
  • 1
  • 10
  • 25

3 Answers3

18

Authorization policy extending [Authorize]

You could do this using authorization policies. Configure these in your Startup.cs inside ConfigureServices(IServiceCollection services) like so:

services.AddAuthorization(options =>
{
    // Create your own policy and make the "access checks" in there
    options.AddPolicy("MyAccessPolicy", policy => policy.RequireAssertion(httpCtx =>
    {
        if (access checks...)
            return true;
        else
            return false;
    }));
});

Then you simply decorate your controller action with the Authorize attribute like so:

[Authorize(Policy = "MyAccessPolicy")]
public IActionResult Access()
{
    return View();
}

Now, whenever you try to go to /access this policy will run, and if the policy returns false, the user will be met with an HTTP 403 (Forbidden) status code.

Custom middleware mapped to route

In response to your comment here's an example of a middleware and how to map it to a specific route.

An example from my own project with a global error handling middleware (some irrelevant parts stripped out):

public class ExceptionHandlingMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            // Call next middleware
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        ErrorDetails error = null;
        if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            error = _localizer.FilesOrFoldersNotFound();
        }
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonConvert.SerializeObject(
            new CustomResponse(false, error ?? _localizer.DefaultError()),
            _serializerSettings));
        }
    }

To only use this middleware for specific routes you could do as suggested here:

// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Map("path/where/error/could/happen",
        b => b.UseMiddleware<ExceptionHandlingMiddleware>());
    // ...
}

Or check the path inside the middleware itself:

// ExceptionHandlingMiddleware.cs
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    if (!context.Request.Path.StartsWithSegments("path/where/error/could/happen"))
    {
        // Skip doing anything in this middleware and continue as usual
        await next(context);
        return;
    }

    // Use middleware logic 
    try
    {
        // Call next middleware
        await next(context);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex);
    }
}
Xerillio
  • 4,855
  • 1
  • 17
  • 28
  • Thanks for you answer, however this would require the Authorize middleware (which I'd prefer to do without). Do you know how I could add custom middleware to the specific route instead? – user2768479 Apr 14 '20 at 21:04
  • @user2768479 Is there a good reason for why you don't want that? I mean what you're asking sounds exactly like a typical case of authorization. – Xerillio Apr 14 '20 at 21:11
  • You are right - it does sound (and is an authenticate/authorize thing) - however I would prefer not using the Auth/Authorize middlewares. I am not trying to reinvent the wheel, I would just like to know how it is done. – user2768479 Apr 17 '20 at 19:29
  • @user2768479 I've added an example of using a custom middleware. I haven't tested the routing part, but something like this should work. – Xerillio Apr 18 '20 at 00:46
9

You can Extending AuthorizeAttribute along with IAuthorizationFilter in Asp.Net Core

1.Create a class which extends AuthorizeAttribute, this will used on top of controller or action like Asp.Net core’s inbuilt [Authorize] attribute.

2.Implement the method OnAuthorization(AuthorizationFilterContext context) which is part of IAuthorizationFilter interface.

3.Call return keyword without any additional operation for authorized user.

4.Set AuthorizationFilterContext result as Unauthorized for unauthorized users as context.Result = new UnauthorizedResult()

    public class SampleAuthorizePermission : AuthorizeAttribute, IAuthorizationFilter
{
    public string Permissions { get; set; }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (string.IsNullOrEmpty(Permissions))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        var userName = context.HttpContext.User.Identity.Name;

        var assignedPermissionsForUser =
            MockData.UserPermissions
                .Where(x => x.Key == userName)
                .Select(x => x.Value).ToList();

        var requiredPermissions = Permissions.Split(",");
        foreach (var x in requiredPermissions)
        {
            if (assignedPermissionsForUser.Contains(x))
                return;
        }

        context.Result = new UnauthorizedResult();
        return;
    }
}

and in your controller

[SampleAuthorizePermission(Permissions = "CanRead")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }
Vahid
  • 474
  • 2
  • 7
  • 20
  • I like the approach, one thing I am unsure of is how this would be approached if you want to also add in any authorization "stuff" from the controller as well, so that the hierarchy feature still works. – perustaja Jan 06 '21 at 22:05
1

Taking the middleware specific approach in .NET Core 3.1, we can conditionally add middleware using the following- In configure method-

app.UseWhen(
    context=>context.Request.Path.StartsWithSegments("your-route-url"),
    branch=>branch.useMiddleware(););

There are a few ways how the pipeline branching can happen, follow the docs for more information- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0#branch-the-middleware-pipeline

Marco
  • 56,740
  • 14
  • 129
  • 152
Dragonknot
  • 292
  • 4
  • 10