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.
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.
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:
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.
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.
If none of the authentication filters set a result, then for every IAuthorizationFilter
its OnAuthorization
method is executed.
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.
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With