MVC 5.0 [AllowAnonymous] and the new IAuthenticationFilter

18,235

Solution 1

In regards to : Question 1) I was under the impression that the [AllowAnonymous] attribute would automagically bypass any code within my CustomAuthenticationAttribute but I was wrong! Do I need to manually check for the existence of the [AllowAnonymous] attribute and skip any code?

As far as I know [AllowAnonymous] attribute has nothing to do with a CustomAuthenticationAttribute. They have different purposes. [AllowAnonymous] would have an effect during an Authorization context, but not in Authentication context.

The Authentication filter has been implemented for setting up authentication context. For instance, AuthenticationContext provides you information for performing authentication. You can use this information to make authentication decisions based on the current context. For example, you may decide to modify the ActionResult to different result type based on the authentication context, or you may decide to change the current principal based on the authentication context etc.

OnAuthenticationChallenge method runs after the OnAuthentication method. You can use OnAuthenticationChallenge method to perform additional tasks on the request.

In regards to : Question 2) Why is the code inside my Index() method of my HomeController gets executed after the OnAuthentication? Only to realize that after I return View() do the code inside the OnAuthenticationChallenge() gets executed?

This is the expected behaviour. Since the you have a Globally registered Authentication filter, the very first thing is that, before any action executes, it would first fire the OnAuthentication event as you would have noticed. Then the OnAuthenticationChallenge after the Index being executed. Once the Action is succeeded any authentication filter relevant that Action (i.e Index), would run the OnAuthenticationChallenge so it can contribute to the action result. As you have in your code for OnAuthenticationChallenge you can modify the ActionResult to an HttpUnauthorizedResult this would get negotiated with the ActionResult.

Solution 2

In answer to Question 1:

The [AllowAnnoymous] attribute acts like a flag (it actually has no implementation logic within it). Its presence is merely checked for by the [Authorize] attribute during execution of OnAuthorization. Decompiling the [Authorize] attribute reveals the logic:

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

        if (skipAuthorization)
        {
            return;
        }

[AllowAnnonymous] would never 'automagically' bypass the code in your custom attribute...

So the answer to the second half of Question 1 is: Yes - if you want your custom attribute to react to the presence of the [AllowAnnonymous], then you would need to implement a check (similar to the above) for the [AllowAnnonymous] attribute in your custom [Authorize] attribute.

Solution 3

I need to provide a clarification here to your second question:

Question 2) Why is the code inside my Index() method of my HomeController gets executed after the OnAuthentication? Only to realize that after I return View() do the code inside the OnAuthenticationChallenge() gets executed?

You should actually be testing for credentials in OnAuthentication if you want to prevent the user from executing the code in your action method. OnAuthenticationChallenge is your chance to handle the 401 with a custom result, such as redirecting the user to a custom controller/action and give them a chance to authenticate.

public class CustomAuthenticationAttribute : ActionFilterAttribute, IAuthenticationFilter 
{
    public void OnAuthentication(AuthenticationContext filterContext)
    {
            var user = filterContext.HttpContext.User;
        if (user == null || !user.Identity.IsAuthenticated)
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
    {
        // modify filterContext.Result to go somewhere special...if you do
        // nothing here they will just go to the site's default login
    }
}

Here is a more complete run-through of the filter and how you might work with it: http://jameschambers.com/2013/11/working-with-iauthenticationfilter-in-the-mvc-5-framework/

Cheers.

Share:
18,235

Related videos on Youtube

Vlince
Author by

Vlince

trying to make a living...

Updated on September 15, 2022

Comments

  • Vlince
    Vlince over 1 year

    When I create a new asp.net mvc 4.0 application, one of the first thing I do, is create and set a custom authorize global filter like so:

    //FilterConfig.cs
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
     //filters.Add(new HandleErrorAttribute());
     filters.Add(new CustomAuthorizationAttribute());
    }
    

    Then I create the CustomAuthorizationAttribute like so:

    //CustomAuthorizationAttribute.cs
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())  
            {
                //Handle AJAX requests
                filterContext.HttpContext.Response.StatusCode = 403;
                filterContext.Result = new JsonResult { JsonRequestBehavior = JsonRequestBehavior.AllowGet };
            }
            else
            {
                //Handle regular requests
                base.HandleUnauthorizedRequest(filterContext); //let FormsAuthentication make the redirect based on the loginUrl defined in the web.config (if any)
            }
        }
    

    I have two controllers: HomeController and SecureController

    The HomeController is decorated with the [AllowAnonymous] attribute.

    The SecureController is NOT decorated with the [AllowAnonymous] attribute.

    The Index() ActionResult of the HomeController displays a View with a simple button.

    When I click the button, I make an ajax call to a GetData() method that lives inside the SecureController like so:

    $("#btnButton").click(function () {
        $.ajax({
            url: '@Url.Action("GetData", "Secure")',
            type: 'get',
            data: {param: "test"},
            success: function (data, textStatus, xhr) {
                console.log("SUCCESS GET");
            }
        });
    });
    

    Needless to say, when I click the button, I trigger the CustomAuthorizationAttribute because it is a global filter but also because the SecureController is NOT decorated with the [AllowAnonymous] attribute.

    Ok, I’m done with my introduction...

    With the introduction of asp.net mvc 5.0, we are now introduced to a new authentication filter which happens to get triggered before the authorization filter (which is great and gives us more granular control on how I can differentiate a user that is NOT authenticated (http 401) from a user that IS authenticated and who happens to NOT be authorized (http 403)).

    In order to give this new authentication filter a try, I’ve created a new asp.net mvc 5.0 (VS Express 2013 for Web) and started by doing the following:

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //filters.Add(new HandleErrorAttribute());
        filters.Add(new CustomAuthenticationAttribute());   //Notice I'm using the word Authentication and not Authorization
    }
    

    Then the attribute:

    public class CustomAuthenticationAttribute : ActionFilterAttribute, IAuthenticationFilter 
    {
        public void OnAuthentication(AuthenticationContext filterContext)
        {
    
        }
    
        public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
        {
            var user = filterContext.HttpContext.User;
            if (user == null || !user.Identity.IsAuthenticated)
            {
                filterContext.Result = new HttpUnauthorizedResult();
            }
        }
    }
    

    I’ve created a HomeController. The HomeController is decorated with the [AllowAnonymous] attribute.

    Before launching the application from VS 2013, I’ve set two break points inside both methods of my CustomAuthenticationAttribute (OnAuthentication and OnAuthenticationChallenge).

    When I launch the application, I hit the first break point(OnAuthentication). Then, to my surprise, the code within the Index() ActionResult of my HomeController gets executed and only after I return the View() do I hit the break point on the OnAuthenticationChallenge() method.

    Questions: I have two questions.

    Question 1)
    I was under the impression that the [AllowAnonymous] attribute would automagically bypass any code within my CustomAuthenticationAttribute but I was wrong! Do I need to manually check for the existence of the [AllowAnonymous] attribute and skip any code?

    Question 2) Why is the code inside my Index() method of my HomeController gets executed after the OnAuthentication? Only to realize that after I return View() do the code inside the OnAuthenticationChallenge() gets executed?

    My concern is that I do not want the code from the Index() method to get executed if the user is NOT authenticated.

    Perhaps I’m looking at this the wrong way.

    If anyone can help me shed some light on this, that’d be great!

    Sincerely Vince

  • UserControl
    UserControl over 10 years
    In my understanding OnAuthentication() must not decide to return HttpUnauthorizedResult - this is authorization.
  • MisterJames
    MisterJames over 10 years
    @UserControl Where did you gain that understanding from? 401 Unauthorized is a poorly aligned term, but it's the way to say "you need different credentials than the ones you're presenting to access this resource". In fact the HTTP RFC says that the server response should be an authentication challenge for the specified resource. How does this not align?
  • UserControl
    UserControl over 10 years
    Well, it's quite clear for me "different credentials" means authorization, because once we can get any valid credentials we can answer the question who is who - exactly what authentication is supposed to do. And, if the credentials provided is not enough to perform some action it's definitely authorization issue (come back and give me another valid credentials).
  • MisterJames
    MisterJames over 10 years
    "...the ones you're presenting..." can also mean "anonymous". The point here is that if you allow an unauthenticated user through OnAuthenticate without changing the result, you allow the user to execute the action, which could be a POST, PUT or DELETE. I don't necessarily disagree with your sentiment, but what would you suggest is the correct HTTP response for an unauthenticated user, if not the HTTP RFC recommended 401?
  • UserControl
    UserControl over 10 years
    I think this is the difference between classic ASP.NET and new pipeline which is the issue. The former (being IHttpModule) executes both AuthenticateRequest and AuthorizeRequest before executing a request. Not sure what's the better but it looks like MVC being hosted by ASP.NET breaks its fundamentals.