63

This has had me stumped for a while. None of the commonly encountered similar situations seem to apply here apparently. I've probably missed something obvious but I can't see it.

In my Mvc Web Application I use the Authorize and AllowAnonymous attributes in such a way that you have to explicitly open up an action as publicly available rather than lock down the secure areas of the site. I much prefer that approach. I cannot get the same behaviour in my WebAPI however.

I have written a custom Authorization Attribute that inherits from System.Web.Http.AuthorizeAttribute with the following:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class MyAuthorizationAttribute : System.Web.Http.AuthorizeAttribute

I have this registered as a filter:

    public static void RegisterHttpFilters(HttpFilterCollection filters)
    {
        filters.Add(new MyAuthorizationAttribute());
    }

This all works as expected, actions are no longer available without credentials. The problem is that now the following method will not allow the AllowAnonymous attribute to do it's thing:

[System.Web.Http.AllowAnonymous]
public class HomeController : ApiController
{
    [GET("/"), System.Web.Http.HttpGet]
    public Link[] Index()
    {
        return new Link[] 
        { 
            new SelfLink(Request.RequestUri.AbsoluteUri, "api-root"),
            new Link(LinkRelConstants.AuthorizationEndpoint, "OAuth/Authorize/", "authenticate"),
            new Link(LinkRelConstants.AuthorizationTokenEndpoint , "OAuth/Tokens/", "auth-token-endpoint")
        };
    }
}

The most common scenario seems to be getting the two Authorize / AllowAnonymous attributes mixed up. System.Web.Mvc is for web apps and System.Web.Http is for WebAPI (as I understand it anyway).

Both of the Attributes I'm using are from the same namespace - System.Web.Http. I assumed that this would just inherit the base functionality and allow me to inject the code I need in the OnAuthotize method.

According to the documentation the AllowAnonymous attribute works inside the OnAuthorize method which I call immediately:

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        base.OnAuthorization(actionContext);

Any thought's would be really appreciated.

Has anyone encountered this problem before and found the root cause?

Jammer
  • 9,969
  • 11
  • 68
  • 115
  • Make sure you use `System.Web.Mvc.AllowAnonymousAttribute` and not `System.Web.Http.AllowAnonymousAttribute`. It happens to me and I realised it three hours later... – ToXinE Feb 09 '16 at 11:02

8 Answers8

101

In the AuthorizeAttribute there is the following code:

private static bool SkipAuthorization(HttpActionContext actionContext)
{
    Contract.Assert(actionContext != null);

    return actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()
               || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
}

Include this method in your AuthorizeAttribute class then add the following to the top of your OnAuthorization method to skip authorization if any AllowAnonymous attributes are found:

if (SkipAuthorization(actionContext)) return;
Richard Garside
  • 87,839
  • 11
  • 80
  • 93
Jammer
  • 9,969
  • 11
  • 68
  • 115
  • 1
    found the same problem when I added a custom AuthorizationFilterAttribute and this solved it. – da_jokker Mar 21 '17 at 22:05
  • 5
    Be aware that this code has counter-intuitive behavior: an `[AllowAnonymous]` attribute on the controller will **override** any authorization attributes on the method. To prevent this, you can add another custom attribute check for the `ActionDescriptor` against `IAuthorizationFilter` (or whatever base class/interface you're using) and only skip authentication if that is false when the anonymous controller is true. – Nick Oct 05 '17 at 17:46
  • 6
    True, but I'd argue that is a bit of a design issue as well. If you are mixing secure and non-secure actions on the same controller it's time to move things around. – Jammer Oct 05 '17 at 17:49
37

ASP.NET MVC 4:

bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
                         || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);

or

 private static bool SkipAuthorization(AuthorizationContext filterContext)
    {
        Contract.Assert(filterContext != null);

        return filterContext.ActionDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any()
               || filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any();
    }

Soruce: http://weblogs.asp.net/jongalloway/asp-net-mvc-authentication-global-authentication-and-allow-anonymous

g.breeze
  • 1,940
  • 19
  • 24
  • 1
    User asked for web api behavior, not mvc. – Leandro Bardelli Mar 30 '15 at 13:29
  • 1
    @LeandroTupone: When you use AllowAnonymousAttribute in the code rather than referencing the System.Web.Mvc, use System.Web.Http; then it should be fine. – curiousBoy Jul 27 '18 at 01:56
  • 2
    I came here hoping I could adapt posters issue to the same issue I am having in MVC. Leaving satisfied thanks to this answer. – Drew Jan 25 '19 at 16:00
20

In my case, none of the above solutions worked. I am using .Net Core 3.1 with a custom IAuthorizationFilter and I had to do the following:

public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any()) return;
Stuyvenstein
  • 2,340
  • 1
  • 27
  • 33
3

Using MVC 5
Steps to overcome this issue:-
1. Update your Anonymous attribute of WebAPI project and make it like

[System.Web.Mvc.AllowAnonymous]
  1. Now go to your custom attribute class and write the code

     public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext filterContext)
    {
        if (filterContext == null)
        {
            throw new UnauthorizedAccessException("Access Token Required");
        }
        base.OnAuthorization(filterContext);
        if (filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())
        {
            return;
        }
        if (filterContext.Request.Headers.Authorization != null)
        {
            var response = 
     PTPRestClient.GetRequest(filterContext.Request.Headers.Authorization.ToString(), 
     "api/validate/validate-request");
            if (!response.IsSuccessStatusCode)
            {
                throw new UnauthorizedAccessException();
            }
    
    
        }
        else
        {
            throw new UnauthorizedAccessException("Access Token Required");
        }
    }
    
2

Using C#6.0 Create a static class that extends the ActionExecutingContext.

public static class AuthorizationContextExtensions {
    public static bool SkipAuthorization(this ActionExecutingContext filterContext) {    
         Contract.Assert(filterContext != null);
         return filterContext.ActionDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any()|| filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any();
    }
}

Now your override filterContext will be able to call the extension method, just make sure they are in the same namespace, or include the proper using statement.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeCustomAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        if (filterContext.SkipAuthorization()) return;// CALL EXTENSION METHOD
         /*NOW DO YOUR LOGIC FOR NON ANON ACCESS*/
    }
}
Mike
  • 1,525
  • 1
  • 14
  • 11
2

Here is a solution for ASP.NET Core 2+ and ASP.NET Core 3+. Add it into IAsyncAuthorizationFilter implementation:

private static bool HasAllowAnonymous(AuthorizationFilterContext context)
{
    var filters = context.Filters;
    return filters.OfType<IAllowAnonymousFilter>().Any();
}

And check like this:

public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
    if(HasAllowAnonymous(context))
        return;
}
Rodion Mostovoi
  • 1,205
  • 12
  • 16
0

I must be using a different version of the .net framework or web api but hopefully this helps someone:

        bool skipAuthorization = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any() || actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
        if (skipAuthorization)
        {
            return;
        }
Action Dan
  • 443
  • 4
  • 10
0
public class MyAuthorizationAuthorize : AuthorizeAttribute, IAuthorizationFilter
{
public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAuthenticated)
            {
                bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) ||
                    filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);

                if (skipAuthorization) return;

            }
            else filterContext.Result = new HttpUnauthorizedResult();
        }
}
Suvenjeet
  • 1
  • 1