3

I'm exploring Minimal APIs in .Net 6, and trying to apply a custom Authorization Filter to the endpoint (via Attributes or Extensions). But it seems to me, I am doing something wrong, or it's simply not designed to work in that way (and it's sad if so). Couldn't find anything in the docs besides the default usage of [Authorize] attribute in Minimal APIs.

Here is the Filter

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
{
    //Checking tokens
}

And if I try to apply it at Controller level, it works fine

[CustomAuthorize]
public class CustomController : ControllerBase
{
    //Necessary routing
}

But if I switch to Minimap APIs notation and try to use attributes

app.MapGet("/customEndpoint", 
        [CustomAuthorize] async ([FromServices] ICustomService customService, Guid id) => 
            await customService.GetCustomStuff(id));

or even an extension method

app.MapGet("/customEndpoint", 
        async ([FromServices] ICustomService customService, Guid id) => 
            await customService.GetCustomStuff(id)).WithMetadata(new CustomAuthorizeAttribute());

It just doesn't work. The filter doesn't even being constructed.

What did I miss or did wrong? Thx in advance

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Beeeg
  • 33
  • 1
  • 4
  • When you use controllers and attributes, the MVC framework is designed to use them the designated way. this does not apply to the custom classes you have created. You might want to research if you can inject the authorization layer in minimal API by some other mean. – Chetan Aug 03 '22 at 10:03
  • https://auth0.com/blog/securing-aspnet-minimal-webapis-with-auth0/ – Chetan Aug 03 '22 at 10:04
  • 1
    Why don't you create custom middleware for this, then that will validate every request – Navoneel Talukdar Aug 03 '22 at 10:32
  • Thx, I've got such middleware, actually, but I was curious about what I can reuse directly from MVC in Minimal APIs – Beeeg Aug 03 '22 at 10:39
  • @Beeeg ASP.NET Core MVC includes _a lot_ of plumbing and features that aren't needed (and so _can't be used_) by minimal APIs - it seems `[Authorize]` is one of those things - _but that's okay_ because honestly the `[Authorize]` attribute itself is pretty awful (while declarative behaviour is nice, there's far too many moving-parts and frustrating restrictions with attribute and declarative-based auth), so you really are much better-off with using middleware for this. – Dai Aug 03 '22 at 11:05

2 Answers2

8

You can write a custom authorization filter for Minimal API in .NET 6.0

Here is how I tend to approach it - by using Policy-based authorization in ASP.NET Core

Step 1: Create a Requirement

A requirement implements IAuthorizationRequirement

public class AdminRoleRequirement : IAuthorizationRequirement
{
     public AdminRoleRequirement(string role) => Role = role;
     public string Role { get; set; }
}

Note: A requirement doesn't need to have data or properties.

Step 2: Create a Requirement Handler

A requirement handler implements AuthorizationHandler<T>

 public class AdminRoleRequirementHandler : AuthorizationHandler<AdminRoleRequirement>
 {
    public AdminRoleRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Value == requirement.Role))
        {
            context.Succeed(requirement);
        }
        else
        {

            _httpContextAccessor.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            _httpContextAccessor.HttpContext.Response.ContentType = "application/json";
            await _httpContextAccessor.HttpContext.Response.WriteAsJsonAsync(new { StatusCode = StatusCodes.Status401Unauthorized, Message = "Unauthorized. Required admin role." });
            await _httpContextAccessor.HttpContext.Response.CompleteAsync();

            context.Fail();

        }

    }
    private readonly IHttpContextAccessor _httpContextAccessor;
}

Note: HandleRequirementAsync method returns no value. The status of either success or failure is indicated by calling context.Succeed(IAuthorizationRequirement requirement) and passing the requirement that has been successfully validated or by calling context.Fail() to indicate AuthorizationHandlerContext.HasSucceeded will never return true, even if all requirements are met.

Step 3: Configure Your Policy in the Authorization Service

 builder.Services.AddAuthorization(o =>
 {
        o.AddPolicy("AMIN", p => p.AddRequirements(new AdminRoleRequirement("AMIN")));
 });

Step 4: Add Your Requirement Handler to DI

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

Step 5: Apply Policy to Endpoints

 app.MapGet("/helloworld", () => "Hello World!").RequireAuthorization("AMIN");
drzaus
  • 24,171
  • 16
  • 142
  • 201
Bloggrammer
  • 921
  • 8
  • 21
  • 1
    The only thing wrong with this is the injecting of the IHttpContextAccessor. Authorization handlers shouldn't be writing response... – davidfowl Aug 28 '22 at 17:03
  • Noted @davidfowl. – Bloggrammer Aug 28 '22 at 17:37
  • returning StatusCode from httpContextAccessor is a little bit hacky. More proper is to add Step 6: AuthorizationMiddlewareResultHandler. Here is good example from @Ogglas: https://stackoverflow.com/questions/35656828/return-http-403-using-authorize-attribute-in-asp-net-core – Niksr Mar 17 '23 at 19:08
2

I think you won't be able to inject action filter in minimal api, you can use 3 alternative approches.

  1. Create a custom middleware and inject it in startup class, it would check every request and do the intended work as you filter is doing. You can put a check for the request path there if you only need to validate a specific controller/endpoint.

  2. The second approach is you can inject httpcontext in minimal api like this, from that extract jwt token and validate that, if found not ok reject that request.


 app.MapGet("/customEndpoint", async (HttpContext context, ICustomService service) =>
 {
     var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
     if (string.isNullOrEmpty(token) || <not a valid token>) return Results.Unauthorized();    
     // do some work 
     return Results.Ok(result);
 });

as @Dai suggested, you can extract token in this way also

AuthenticationHeaderValue.TryParse(context.Request.Headers["Authorization"], out var parsed ) && parsed.Scheme == "BearerOrWhatever" ? parsed.Parameter : null
  1. You can register the filter globally from startup.cs.
Navoneel Talukdar
  • 4,393
  • 5
  • 21
  • 42