0

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.

Savage
  • 2,296
  • 2
  • 30
  • 40
  • Just to confirm, a simple `[AllowAnonymous]` on an Action method does not override the Authorization attribute in your case? – Marco Jan 22 '18 at 12:56
  • Confirmed. If I debug, I can see it's hitting the controller attribute and getting stuck in a redirect loop. – Savage Jan 22 '18 at 12:58
  • 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 redirect – Marco Jan 22 '18 at 12:58
  • 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 Jan 22 '18 at 13:13
  • You can also try [this](https://stackoverflow.com/a/16713334/2218697) answer. May be helpful. – Shaiju T Jan 22 '18 at 13:17
  • Using .Net 4.5.2. I've tried inheriting from both versions of `AuthorizeAttribute` (Http and Mvc) without success. – Savage Jan 22 '18 at 13:35
  • We really could do with your full implementation of your AuthorizePublic class to reproduce this. Or a dumbed down version of it in any case. At least something we can copy paste into our own sandboxes to see whats what. – Marco Jan 22 '18 at 13:37
  • You can just put `return true` in there. The problem is that the attribute is not being hit at action level. Additionally it seems that there are two AuthorizeAttribute classes you can inherit from, so the various code examples differ. – Savage Jan 22 '18 at 13:52
  • In other words, the fact there is a redirect loop is a red herring - the real issue is that I'm unable to override the custom attribute defined on the controller, which is why it's looping on itself. – Savage Jan 22 '18 at 13:55

1 Answers1

4

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.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • A detailed explanation, thanks - overriding the reading of the attributes did the trick for me – Savage Jan 23 '18 at 07:49