I'm implemented a [CustomAuthorization]
attribute based on [Authorize]
attribute. My attribute looks like this:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public eUserRole CustomRoles { get; set; } = eUserRole.Administrator; // If not specified, the required role is Administrator
protected override bool IsAuthorized(HttpActionContext actionContext)
{
AuthorizationSystem auth = new AuthorizationSystem(actionContext.RequestContext.Principal, this.CopyleaksRoles);
var res = auth.Validate();
if (!res)
return false;
return base.IsAuthorized(actionContext);
}
}
I splitted the logic (who to accept and who not) to seperated class. The method AuthorizationSystem.Validate()
return true if the user is accepted according to his CustomRoles
property.
My Controller looks like:
[CustomAuthorize]
public class MyController : ApiController
{
[CustomAuthorize(CustomRoles = eUserRole.Readonly)]
public Response Do()
{
// ... Do something ...
}
}
I'm running the application (C# + WebAPI) to check if it working.
I debugging the code and see that on the first run the minimum required role level is Administrator
instead of Readonly
. Because when using [CustomAuthorize]
without any CustomRoles
, it's define the default row to be eUserRole.Administrator
. That mean that the first CustomAuthorize
attribute that being called is the attribute on class level, not on method level.
How to make it call the attribute that on the method (Do()
) before?
You are getting confused by the fact that AuthorizeAttribute
implements both Attribute
and IAuthorizationFilter
. What you need to do is make a globally registered IAuthorizationFilter
and use it to determine whether the CustomAuthorizeAttribute
exists on the action or controller. Then you can determine which takes precedence over the other.
CustomAuthorizeAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CustomAuthorizeAttribute : Attribute
{
public eUserRole CustomRoles { get; set; } = eUserRole.Administrator;
}
CustomAuthoizationFilter
Here we save some steps by subclassing AuthorizeAttribute
, but we don't intend for this to be an Attribute
at all, only a globally registered filter.
Our filter contains Reflection code to determine which CustomAuthorize
attribute takes precedence if both are defined. This is setup to make the action method override the controller, but you can make the logic more complex, if needed.
public class CustomAuthorizationFilter : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (base.IsAuthorized(actionContext))
{
var authorizeAttribute = GetAuthorizeAttribute(actionContext.ActionDescriptor);
// Attribute doesn't exist - return true
if (authorizeAttribute == null)
return true;
var roles = authorizeAttribute.CustomRoles;
// Logic - return true if authorized, false if not authorized
}
return false;
}
private CustomAuthorizeAttribute GetAuthorizeAttribute(HttpActionDescriptor actionDescriptor)
{
// Check action level
CustomAuthorizeAttribute result = actionDescriptor
.GetCustomAttributes<CustomAuthorizeAttribute>()
.FirstOrDefault();
if (result != null)
return result;
// Check class level
result = actionDescriptor
.ControllerDescriptor
.GetCustomAttributes<CustomAuthorizeAttribute>()
.FirstOrDefault();
return result;
}
}
Usage
We register the filter globally. For each request, CustomAuthorizationFilter
scans the action and controller in the request to see if the attribute exists. If so, it then runs the logic.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Register our Custom Authorization Filter
config.Filters.Add(new CustomAuthorizationFilter());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
NOTE: Technically you can keep them both in the same class, but it is less confusing if you separate them into the components that that actually are rather than making a single class that does more than one job (attribute and filter).
Reference: Passive Attributes