I have this attribute:

public class ValidateCertAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext context)
    {
        // Read certificate from the HTTP Request and
        // check its various attributes against known
        // values from a config file.
        if (true) // certificate is invalid
        {
            context.Response = new HttpResponseMessage(HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "Invalid certificate"
            };
        }
        else
        {
            base.OnAuthorization(context);
        }
    }
}

and this action:

[HttpGet]
[Route("TestAuth")]
[ValidateCert]
public HttpResponseMessage TestAuth()
{
    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        ReasonPhrase = "In Test method without any authorization."
    };
}

This action is inside a .NET Core Web Api controller:

[ApiController]
public class TestAuthController : ControllerBase

On the other hand, Startup.cs contains:

app.UseMvc();

so that seems odd, since this should just be a Web Api controlller, and not an MVC web application. Apparently, though, that's just for routing, but I thought it was worth mentioning.

I've set a breakpoint on the first line of the attribute code, but it's not getting hit. I believe it should get hit before the action executes and that the action should never execute because I'm setting the Response inside the attribute. Why isn't the attribute executing?

1 Answers

1
JuanR On Best Solutions

This can be accomplished using policy-based authorization.

The idea essentially is that you have a requirement that needs to be met (valid certificate), a handler for the requirement (how to validate the certificate) and a policy that enforces this requirement and is applied during authorization.

If your code is simple enough, you can just provide a Func<AuthorizationHandlerContext, bool> to the policy that is applied for evaluation. Here is how to setup the policy (in Startup.cs, ConfigureServices()):

services.AddAuthorization(options =>
{
    options.AddPolicy("ValidateCertificate", policy =>
       policy.RequireAssertion(context => 
       {
           var filterContext = (AuthorizationFilterContext)context.Resource;
           var Response = filterContext.HttpContext.Response;
           var message = Encoding.UTF8.GetBytes("Invalid certificate");
           Response.OnStarting(async () =>
           {
               filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
               await Response.Body.WriteAsync(message, 0, message.Length);
           });                       
           return false;
       }));
});

This will do what you want.

Now, if you want to go the more structured route, you can implement each piece:

First, create your requirement (more like a marker for reference, really):

public class ValidCertificateRequirement : IAuthorizationRequirement
{

}

Then setup the policy that needs to be applied (Startup.cs, ConfigureServices()):

services.AddAuthorization(options => 
{
    options.AddPolicy("ValidateCertificate", policy => 
    {
        policy.Requirements.Add(new ValidCertificateRequirement());
    });
});

Now you need to create your requirement handler:

public class ValidCertificateHandler : AuthorizationHandler<ValidCertificateRequirement>
{
    public ValidCertificateHandler()
    {
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidCertificateRequirement requirement)
    {
        var filterContext = (AuthorizationFilterContext)context.Resource;
        var Response = filterContext.HttpContext.Response;
        var message = Encoding.UTF8.GetBytes("Invalid certificate");
        Response.OnStarting(async () =>
        {
            filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            await Response.Body.WriteAsync(message, 0, message.Length);
        });
        context.Fail();
        return Task.CompletedTask;
    }
}

The handler then needs to be registered on startup (in ConfigureServices()):

//Register handler
services.AddSingleton<IAuthorizationHandler, ValidCertificateHandler>();

Lastly, for either approach (assertion or implementation), apply the Authorize attribute to your actions, indicating the policy to apply:

[Authorize(Policy = "ValidateCertificate")]
public HttpResponseMessage TestAuth()
{
    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        ReasonPhrase = "In Test method without any authorization."
    };
}

You can read more about it here:

Policy-based authorization in ASP.NET Core