Reputation: 360
I thought I had a very simple goal, a custom attribute to be assigned per-endpoint (ApiController
method) that could be used for endpoint-specific authorization purposes, effectively authorizing an endpoint with some additional metadata provided at design time.
For example:
[HttpGet]
[MyCustomAttribute("ABCD")]
public async Task<ActionResult> SomeAction()
{
// Do your stuff in here
}
In this case I want to authorize this endpoint for use based on things like the current identity for the logged in user (authentication is already performed and an identity is set already with various claims), as well as consider the value in the custom attribute.
Searching online is difficult because there's so many different classes and it seems every .net version there's a new way to do this. The one I found that made the most sense is something like this, first the attribute:
internal class MyCustomAttribute: AuthorizeAttribute, IAuthorizationRequirement
{
public MyCustomAttribute(string customValue) => MyCustomValue = customValue;
public string MyCustomValue { get; set; }
}
Then I create a new class inheriting from AuthorizationHandler
:
internal class MyCustomAttributeAuthorizationHandler : AuthorizationHandler<MyCustomAttribute>
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IApiSecurityRepository repository;
public MyCustomAttributeAuthorizationHandler (IHttpContextAccessor httpContextAccessor, IApiSecurityRepository repository)
{
this.httpContextAccessor = httpContextAccessor;
this.repository = repository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyCustomAttribute requirement)
{
// In here now I can perform my logic for authentication, using the custom attribute
// (I can access requirement.MyCustomValue)
// as well as accessing the services I've DI'd via constructor
// (I can use repository.GetXXX() to make a required query or similar)
}
}
This was the only way of setting it up that I found online (I tried filters and policies, etc) that allowed me to have a custom attribute (parameterized) as well as be able to DI other services that I required.
Now I've registered it as such:
builder.Services.AddScoped<IAuthorizationHandler, MyCustomAttributeAuthorizationHandler>();
But I'm not sure what else I need, as this doesn't seem to be sufficient to make use of this handler.
I tried something along the lines of:
builder.Services.AddAuthorization(c => { c.AddPolicy("MyCustomAttribute", ??) });
But here I don't know what or how to create an AuthorizationPolicy
to plug in. I was hoping there was some way to inject the handler itself and make use of it without additional creation of policies, not totally sure how this works. I've done plenty of research but literally every single result on google/SO has a completely different implementation with different classes and techniques for registering them. This code doesn't have to be production-level code, it's more internal, but I'd really like to get this authorization working.
Thanks!
Upvotes: 2
Views: 1276
Reputation: 37337
I have managed to implement your logic correctly using standard authorization interfaces and classes in ASP.NET Core by following these articles
So, first of all, i have separated requirement from attribute:
public class MyCustomAttribute : AuthorizeAttribute
{
public MyCustomAttribute(string customValue)
{
// The prefix is defined below
Policy = MyCustomPolicyProvider.CustomPolicyPrefix + customValue;
}
}
public class MyCustomRequirement : IAuthorizationRequirement
{
public MyCustomRequirement(string myCustomValue)
{
MyCustomValue = myCustomValue;
}
public string MyCustomValue { get; }
}
Missing part of your solution was policy provider, which can be seen below:
public class MyCustomPolicyProvider : DefaultAuthorizationPolicyProvider
{
public const string CustomPolicyPrefix = "MY_CUSTOM_PERMISSION_";
public MyCustomPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
{
}
public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(CustomPolicyPrefix))
{
return await base.GetPolicyAsync(policyName);
}
var requirement = new MyCustomRequirement(
policyName.Replace(CustomPolicyPrefix, string.Empty));
return new AuthorizationPolicyBuilder()
.AddRequirements(requirement)
.Build();
}
}
Then you need to register everything:
// Register our custom Authorization handler
builder.Services.AddSingleton<IAuthorizationHandler, MyCustomAttributeAuthorizationHandler>();
// Overrides the DefaultAuthorizationPolicyProvider with our own
builder.Services.AddSingleton<IAuthorizationPolicyProvider, MyCustomPolicyProvider>();
And then you should be just fine.
One issue that may occur is that services inside MyCustomAttributeAuthorizationHandler
must be singletons - IHttpContextAccessor
can be registered as such:
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
When it comes to your service IApiSecurityRepository
, either register it as signleton, or manually create scope with IServiceProvder
and resolve it within that scope.
Upvotes: 0
Reputation: 360
Ended up figuring this out by using an IActionFilter (or IAsyncActionFilter in my case) and just pulling up the attribute and associated metadata through the context. A truncated sample:
public class MyCustomActionFilter : IAsyncActionFilter
{
private readonly IApiSecurityRepository repository;
public MyCustomActionFilter(IApiSecurityRepository repository)
{
this.repository = repository;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// Can do some stuff in here...
// Get the Endpoint...
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint == null)
{
context.Result = new UnauthorizedResult();
return;
}
// Grab the [MyCustomAttribute] descriptor...
var descriptor = endpoint.Metadata.GetMetadata<MyCustomAttribute>();
if (descriptor?.MyCustomValue == null)
{
// I think we'll require this attribute on all endpoints?
context.Result = new UnauthorizedResult();
return;
}
context.Result = new OkResult();
}
}
Then I just added it as a filter during the services setup:
options.Filters.Add<AuthorizedRouteActionFilter>();
Upvotes: 2
Reputation: 8335
You could implement custom authorization attribute to inherit from IAuthorizationFilter
.Such as following
public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
{
private readonly string _claimValue;
private readonly IApiSecurityRepository _repository;
public CustomAuthorizeAttribute(string claimValue, IApiSecurityRepository repository)
{
_claimValue = claimValue;
this._repository = repository;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated ||
!user.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == _claimValue))
{
context.Result = new ForbidResult();
}
}
}
Then [MyCustomAttribute("ABCD")]
will check if user has the role "ABCD". You needn't register policy to builder.
Upvotes: 0