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.
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.
Related
Having set up a default ASP.Net MVC 5 application, I fail to understand why the below snippet has a call to SignInManager.
This is in the ManageController, which has the [Authorize] attribute.
//
// POST: /Manage/RemoveLogin
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> RemoveLogin(string loginProvider, string providerKey)
{
ManageMessageId? message;
var result = await UserManager.Get().RemoveLoginAsync(User.Identity.GetUserId<int>(), new UserLoginInfo(loginProvider, providerKey));
if (result.Succeeded)
{
var user = await UserManager.Get().FindByIdAsync(User.Identity.GetUserId<int>());
if (user != null)
{
await SignInManager.Get().SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
message = ManageMessageId.RemoveLoginSuccess;
}
else
{
message = ManageMessageId.Error;
}
return RedirectToAction("ManageLogins", new { Message = message });
}
I am wondering if, whenver I retrieve the authenticated user, I should repeat this step, i.e. check if the user is null and if not, await SignInAsync.
Edit: check if the user is null and if it is, await SignInAsync
Now, I've created a new controller, that I've given the [Authorize] attribute, and in the Index() function of the controller, I do:
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId<int>());
If I load that page in two tabs, sign out in one of them and then refresh the page, I'm redirected to the login screen. I've attached a debugger and have been unable to cause a case where the SignInManager is hit.
In what scenario would the user be not-null?
They do entirely different things. The Authorize attribute checks that the user is authenticated and authorized to access the action (based on role permissions and such). It doesn't authenticate the user. That's what the call to SignInManager is for. It actually authenticates the user, so that the user can then pass checks made by the Authorize attribute.
As for when the user might be null, effectively, if the action is protected by Authorize it probably never will be. It would have to take some strange confluence of events where the user was found in the database in order to sign them in, but then somehow was removed by the time you tried to retrieve it. In other words: not bloody likely. Still, it's good practice to always perform a null-check like this when a value could potentially be null, which user could.
After going this excellent blog post by Simon I came to know that model binding happens earlier than filters execution (even before authorization filters). If the request is not authorized then it should be rejected as earlier as possible and in that case I prefer running the authorization filters before the model binding process. Also, by this way we could save the time avoiding scanning the request, creating model instances and performing validation.
Is there any reason that I simply don't understand why the MVC request processing pipeline is designed such a way that the model binding should happen before filters?
In asp.net mvc3, the authorization filters execute before the model binding, not after (see code below).
Model binding occurs before the filters because the ActionExecutingContext (parameter of IActionFilter.OnActionExecuting) contains the action's parameters. Maybe they should have lazy loaded those parameters.
The following code is from the System.Web.Mvc.ControllerActionInvoker.
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
{
// code removed for brevity
try
{
// Notice the authorization filters are invoked before model binding
AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
if (authContext.Result != null) {
// the auth filter signaled that we should let it short-circuit the request
InvokeActionResult(controllerContext, authContext.Result);
}
else {
if (controllerContext.Controller.ValidateRequest) {
ValidateRequest(controllerContext);
}
// GetParameterValues does the model binding
IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
}
}
// code removed for brevity
}
Technologies I'm Using:
MVC v2
Forms Authentication (Sliding Expiration)
Session State Server
Custom Authorization Attribute
I'm using the state server process for my mvc app. During testing, when an authenticated user would click the "LogOff" button, it would correctly take them to the authentication screen, and upon successful credential entering, would log them back in. BUT, it would find their prior session variable state, and NOT reload any new permissions I'd given them. This is due to how I'm loading a user in the following code:
public override void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null)
throw new ArgumentNullException("FilterContext");
if (AuthorizeCore(filterContext.HttpContext)) {
IUser customUser = filterContext.HttpContext.Session["CustomUser"] as IUser;
if ((customUser == null) || (customUser.Name != filterContext.HttpContext.User.Identity.Name)) {
customUser = new User(filterContext.HttpContext.User.Identity.Name,
filterContext.HttpContext.User.Identity.IsAuthenticated);
}
if (_privileges.Length > 0) {
if (!customUser.HasAtLeastOnePrivilege(_privileges))
filterContext.Result = new ViewResult { ViewName = "AccessDenied" };
}
filterContext.HttpContext.Session["CustomUser"] = customUser;
}
}
So, you can see I'm storing my customUser in the Session and that value is what was fetched from the prior session even though the user had logged off between (but logged back on within the sliding expiration window.
So, my question is, should I place a simple Session.Abandon() call in my LogOff method in the AccountController, or is there a cleaner more advantageous way of handling this?
Normally Session.Clear() should be enough and remove all values that have been stored in the session. Session.Abandon() ends the current session. It might also fire Session_End and the next request will fire Session_Start.
I have a custom attribute that checks conditions and redirects the user to parts of the application as is necessary per business requirements. The code below is typical:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// ...
if (condition)
{
RouteValueDictionary redirectTargetDictionary = new RouteValueDictionary();
redirectTargetDictionary.Add("action", "MyActionName");
redirectTargetDictionary.Add("controller", "MyControllerName");
filterContext.Result = new RedirectToRouteResult(redirectTargetDictionary);
}
// ...
base.OnActionExecuting(filterContext);
}
I was just asked to allow the user to choose a default page that they arrive at upon logging in. Upon adding this feature, I noticed that the user can get some unusual behavior if there is no action/controller corresponding to the user's default page (i.e. if the application were modified). I'm currently using something like the code below but I'm thinking about going to explicit actions/controllers.
else if (condition)
{
var path = "~/MyControllerName/MyActionName";
filterContext.Result = new RedirectResult(path);
}
How do I check the validity of the result before I assign it to filterContext.Result? I want to be sure it corresponds to a working part of my application before I redirect - otherwise I won't assign it to filterContext.Result.
I don't have a finished answer, but a start would be to go to the RouteTable, get the collection, call GetRouteData with a custom implementation of HttpContextBase to get the RouteData. When done, if not null, check if the Handler is an MvcRouteHandler.
When you've got so far, check out this answer :)
By default, ASP.NET's membership provider redirects to a loginUrl when a user is not authorized to access a protected page.
Is there a way to display a custom 403 error page without redirecting the user?
I'd like to avoid sending users to the login page and having the ReturnUrl query string in the address bar.
I'm using MVC (and the Authorize attribute) if anyone has any MVC-specific advice.
Thanks!
I ended up just creating a custom Authorize class that returns my Forbidden view.
It works perfectly.
public class ForbiddenAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext))
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else
{
// auth failed, display 403 page
filterContext.HttpContext.Response.StatusCode = 403;
ViewResult forbiddenView = new ViewResult();
forbiddenView.ViewName = "Forbidden";
filterContext.Result = forbiddenView;
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
}
Asp.net has had what I consider a bug in the formsauth handling of unauthenticated vs underauthenticated requests since 2.0.
After hacking around like everyone else for years I finally got fed up and fixed it. You may be able to use it out of the box but if not I am certain that with minor mods it will suit your needs.
be sure to report success or failure if you do decide to use it and I will update the article.
http://www.codeproject.com/Articles/39062/Salient-Web-Security-AccessControlModule.aspx