Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing MVC 5 IAuthenticationFilter

Tags:

asp.net-mvc

I don't understand the purpose/difference of OnAuthentication and OnAuthenticationChallenge aside from OnAuthentication running before an action executes and OnAuthenticationChallenge runs after an action executes but before the action result is processed.

It seems as though either one of them (OnAuthentication or OnAuthenticationChallenge) can do all that is needed for authentication. Why the need for 2 methods?

My understanding is OnAuthentication is where we put the logic of authenticating (or should this logic be in actual action method?), connecting to data store and checking for the user account. OnAuthenticationChallenge is where we redirect to login page if not authenticated. Is this correct? Why can't I just redirect on OnAuthentication and not implement OnAuthenticationChallenge. I know there is something I am missing; could someone explain it to me?

Also what is the best practice to store an authenticated user so that succeeding requests wouldn't have to connect to db to check again for user?

Please bear in mind that I am new to ASP.NET MVC.

like image 438
g_b Avatar asked Sep 03 '14 10:09

g_b


People also ask

How did you implement authentication and authorization filter?

Let's Add Authentication Filter in for doing that just right click on Filters folder then select Add ➜ and inside that select Class a new dialog will pop up with name Add New Item with default Class template selected. Then we are going to name class as UserAuthenticationFilter and finally click on Add Button.


1 Answers

Those methods are really intended for different purposes:

  • IAuthenticationFilter.OnAuthentication should be used for setting the principal, the principal being the object identifying the user.

You can also set a result in this method like an HttpUnauthorisedResult (which would save you from executing an additional authorization filter). While this is possible, I like the separation of concerns between the different filters.

  • IAuthenticationFilter.OnAuthenticationChallenge is used to add a "challenge" to the result before it is returned to the user.

  • This is always executed right before the result is returned to the user, which means it might be executed at different points of the pipeline on different requests. See the explanation of ControllerActionInvoker.InvokeAction below.

  • Using this method for "authorization" purposes (like checking if a user is logged in or in a certain role) might be a bad idea since it might get executed AFTER the controller action code, so you might have changed something in the db before this gets executed!

  • The idea is that this method can be used to contribute to the result, rather than perform critical authorization checks. For example you could use it to convert an HttpUnauthorisedResult into a redirect to different login pages based on some logic. Or you could hold some user changes, redirect him to another page where you can request additional confirmation/information and depending on the answer finally commit or discard those changes.

  • IAuthorizationFilter.OnAuthorization should still be used to perform authentication checks, like checking if the user is logged in or belongs to a certain role.

You can get a better idea if you check the source code for ControllerActionInvoker.InvokeAction. The following will happen when executing an action:

  1. IAuthenticationFilter.OnAuthentication is called for every authentication filter. If the principal is updated in the AuthenticationContext, then both context.HttpContext.User and Thread.CurrentPrincipal are updated.

  2. If any authentication filter set a result, for example setting a 404 result, then OnAuthenticationChallenge is called for every authentication filter, which would allow changing the result before being returned. (You could for example convert it into a redirect to login). After the challenges, the result is returned without proceeding to step 3.

  3. If none of the authentication filters set a result, then for every IAuthorizationFilter its OnAuthorization method is executed.

  4. As in step 2, if any authorization filter set a result, for example setting a 404 result, then OnAuthenticationChallenge is called for every authentication filter. After the challenges, the result is returned without proceeding to step 3.

  5. If none of the authorization filters set a result, then it will proceed to executing the action (Taking into account request validation and any action filter)

  6. After action is executed and before the result is returned, OnAuthenticationChallenge is called for every authentication filter

I have copied the current code of ControllerActionInvoker.InvokeAction here as a reference, but you can use the link above to see the latest version:

public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }

    Contract.Assert(controllerContext.RouteData != null);
    if (String.IsNullOrEmpty(actionName) && !controllerContext.RouteData.HasDirectRouteMatch())
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
    }

    ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
    ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);

    if (actionDescriptor != null)
    {
        FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);

        try
        {
            AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor);

            if (authenticationContext.Result != null)
            {
                // An authentication filter signaled that we should short-circuit the request. Let all
                // authentication filters contribute to an action result (to combine authentication
                // challenges). Then, run this action result.
                AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                    controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                    authenticationContext.Result);
                InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result);
            }
            else
            {
                AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
                if (authorizationContext.Result != null)
                {
                    // An authorization filter signaled that we should short-circuit the request. Let all
                    // authentication filters contribute to an action result (to combine authentication
                    // challenges). Then, run this action result.
                    AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                        controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                        authorizationContext.Result);
                    InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result);
                }
                else
                {
                    if (controllerContext.Controller.ValidateRequest)
                    {
                        ValidateRequest(controllerContext);
                    }

                    IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
                    ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);

                    // The action succeeded. Let all authentication filters contribute to an action result (to
                    // combine authentication challenges; some authentication filters need to do negotiation
                    // even on a successful result). Then, run this action result.
                    AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                        controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                        postActionContext.Result);
                    InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters,
                        challengeContext.Result ?? postActionContext.Result);
                }
            }
        }
        catch (ThreadAbortException)
        {
            // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
            // the filters don't see this as an error.
            throw;
        }
        catch (Exception ex)
        {
            // something blew up, so execute the exception filters
            ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
            if (!exceptionContext.ExceptionHandled)
            {
                throw;
            }
            InvokeActionResult(controllerContext, exceptionContext.Result);
        }

        return true;
    }

    // notify controller that no method matched
    return false;
}

As for not hitting the db on every request when setting the principal, you could use some sort of server side caching.

like image 99
Daniel J.G. Avatar answered Sep 21 '22 14:09

Daniel J.G.