0
votes

I have created a custom authorize attribute, but I need some actions to allow anonymous access. I've tried three different approaches without success: use AllowAnonymous, update the existing attribute with additional parameters, and create a new overriding attribute. Basically it seems that the controller-level attribute always gets called before the action-level attribute.

Here's the controller:

[AuthorizePublic(Sites = AuthSites.Corporate)]
public class CorporateController : SecuredController
{
    [AuthorizePublic(Sites = AuthSites.Corporate, AllowAnonymous = true)]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }
}

And the attribute:

public class AuthorizePublic : AuthorizeAttribute
{
    public AuthSites Sites { get; set; }
    public bool AllowAnonymous { get; set; }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        // Logic
    }        
}

As a last resort I can move the login actions onto their own controller, but before I do that, am I missing something to get one of these approaches to work? I'm a bit surprised that action-level attributes aren't overriding controller-level attributes.

1
Just to confirm, a simple [AllowAnonymous] on an Action method does not override the Authorization attribute in your case?Marco
Confirmed. If I debug, I can see it's hitting the controller attribute and getting stuck in a redirect loop.Savage
So the problem seems not to be the Authorization, but the redirect loop - which should not happen. Care to fill in some details on that in your question? Maybe shopw us the code of your AuthorizeCore method, or whatever is causing that redirectMarco
I hear your theory, but I just removed the attribute on the controller, and the custom attribute on the action is being completely ignored. Maybe I need to update my attribute implementation to indicate that it can be used for actions as well?Savage
You can also try this answer. May be helpful.Shaiju T

1 Answers

3
votes

It is the implementation of the OnAuthorization method of AuthorizeAttribute that scans for AllowAnonymousAttribute. So, you must either not override this method or re-implement this check if you want that part to work. Since you have only provided a cut-down implementation of AuthorizeAttribute, it cannot be assumed that you are not overriding this method (and thus overriding the logic that makes the check).

Also, your example controller doesn't actually show usage of the AllowAnonymousAttribute. Instead, it sets a property named AllowAnonymous. If you expect anonymous users to reach that action method, you should decorate it with the attribute that MVC is actually scanning for.

[AuthorizePublic(Sites = AuthSites.Corporate)]
public class CorporateController : SecuredController
{
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }
}

Alternatively, if you need to customize the AllowAnonymous behavior in some way, you can keep using the property you have, but you have to implement the Reflection code yourself to scan for AuthorizePublic and check the AllowAnonymous property.

public class AuthorizePublic : AuthorizeAttribute
{
    public AuthSites Sites { get; set; }
    public bool AllowAnonymous { get; set; }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
        if (actionDescriptor != null)
        {
            AuthorizePublic attribute = GetAuthorizePublicAttribute(actionDescriptor);
            if (attribute.AllowAnonymous)
                return true;

            var sites = attribute.Sites;

            // Logic
        }
        return true;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // Pass the current action descriptor to the AuthorizeCore
        // method on the same thread by using HttpContext.Items
        filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
        base.OnAuthorization(filterContext);
    }

    // Gets the Attribute instance of this class from an action method or contoroller.
    // An action method will override a controller.
    private AuthorizePublic GetAuthorizePublicAttribute(ActionDescriptor actionDescriptor)
    {
        AuthorizePublic result = null;

        // Check if the attribute exists on the action method
        result = (AuthorizePublic)actionDescriptor
            .GetCustomAttributes(attributeType: typeof(AuthorizePublic), inherit: true)
            .SingleOrDefault();

        if (result != null)
        {
            return result;
        }

        // Check if the attribute exists on the controller
        result = (AuthorizePublic)actionDescriptor
            .ControllerDescriptor
            .GetCustomAttributes(attributeType: typeof(AuthorizePublic), inherit: true)
            .SingleOrDefault();

        return result;
    }
}

AuthorizeAttribute implements both Attribute and IAuthorizationFilter. With that in mind, the IAuthorizationFilter part of AuthorizeAttribute is a different runtime instance of the class than the Attribute part. So the former must use Reflection to read the property of the latter in order for it to work. You can't just read the AllowAnonymous property from the current instance and expect it to work, because you are setting the value in the attribute and the code is executing in the filter.

MVC and Web API are completely separate frameworks with their own separate configuration even though they can co-exist in the same project. MVC will completely ignore any controllers or attributes defined in Web API and vise versa.