I have a custom authorize attribute used for Ajax requests:
public class AjaxAuthorize : AuthorizeAttribute {
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) {
UrlHelper urlHelper;
if (filterContext.HttpContext.Request.IsAjaxRequest()) {
urlHelper = new UrlHelper(filterContext.RequestContext);
filterContext.HttpContext.Response.StatusCode = 401;
//Return JSON which tells the client where the login page exists if the user wants to authenticate.
filterContext.HttpContext.Response.Write(new JavaScriptSerializer().Serialize(
new {
LoginUrl = string.Format("{0}?ReturnURL={1}", FormsAuthentication.LoginUrl, urlHelper.Encode(filterContext.HttpContext.Request.Url.PathAndQuery))
}
));
filterContext.HttpContext.Response.End();
} else {
base.HandleUnauthorizedRequest(filterContext);
}
}
}
When I run the application locally I get the JSON result back from the Ajax request. However, when I put the code on my beta server I end up getting the IIS 401 HTML response.
Does anyone see something wrong with my code that would make this work only on localhost? Additionally, if anyone has a better idea for returning the JSON result I am open to that as well.
There is some strange power of StackOverflow that results in the OP thinking through the question differently after posting. I'll leave my answer here in hopes that it might benefit someone else.
It just occurred to me that IIS7 was getting in the way. I fixed this by adding one line of code:
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
Related
I get the following default standard error message with a bad request response whenever the query parameter page is not valid.
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-ase4556503808167887c13a5978d0b-88001bf112e7555d-00","errors":{"page":["The value ''
is invalid."]}}
Similarly when the request body is invalid Json , the framework responds with similar error message as well.
I am catching all the exceptions that happen application wide using an ExceptionFilter.
How would I capture these particular bad request responses and format them and respond back with custom error format? What kind of Filter, Middleware or ModelBinder should I be using ?
Thank you all for the help, I used a combination of the above comments and were able to solve the issue. It is not ideal but this worked well.
The first thing is to disable the automatic model binder filter like this in your StartUp.cs
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
Then, I created an action filter that will execute when ever there is invalid parameter comes from the request:
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
var messages = new List<string>();
actionContext.ModelState.ToList().ForEach(argument =>
{
if (argument.Value?.ValidationState == ModelValidationState.Invalid)
{
argument.Value.Errors.ToList().ForEach(error =>
{
messages.Add(error.ErrorMessage);
});
}
});
actionContext.Result = new BadRequestObjectResult(messages);
}
}
This will format the response removing all the trace id and urls and only display the message you are looking for, instead of messages you can always create your error object and assign that.
In my MVC-Application I have my own error handling that catches different errors. If an authentication error occurs it redirects to a loginpage:
public class JsonExceptionAttribute : HandleErrorAttribute {
if (filterContext.Exception.Message == "Auth expired") {
public override void OnException(ExceptionContext filterContext) {
string newUrl = Logic.GetUrl());
filterContext.Result = new RedirectResult(newUrl, false);
filterContext.ExceptionHandled = true;
return;
}
}
}
GetUrl() returns a valid Url.
This seems to work fine most of the time. It always returns a 302 redirect that will then call the new URL as a GET request.
But this does not work everywhere. If the Exception is thrown in my AuthorizeAttribute the 302 is still returned, but the Browser tries to open the Url with the httpmethod "OPTIONS" instead of GET causing an error:
If I take the exact url and copy/paste it into the browser (so making a GET from it) it works fine. So where is this "OPTIONS" coming from all of a sudden?
This happens in different browsers and the result is the same when I return the Redirect in the HandleUnauthorizedRequest in the AuthorizeAttribute instead of doing this in the HandleErrorAttribute
The EU General Data Protection Regulation (GDPR) will come into effect from 25th May 2018. One can read in detail here. This time it has to be all opt-in and they have very heavy fine (€20 million or 4% of global earning!).
Since, it has to be all opt-in(at least in our case), we have decided user accepts our cookies to receive our services.
We will not be logging out current users to give us concept, however, we will present them consent page when they come into one of our sites. If they say yes then we will save an "accept-cookie" or else they won't be able to come into our sites. Afterwards, whenever a use logs into our site, we check the existence of this cookie.
My idea in implementing this solution is to intercept the user request and check the existence of accept-cookie and redirect to the requested resource or controller in our case as we will asp.net mvc accordingly.
My question is can I do this using RegisterRoutes to route request to a controller and if yes, redirect to the requested controller?
What about this solution? Though, the solution is for different aspect. I have modified the variables name from language to consent to make it more meaningful(not trying to copy):
public class EnsureLanguagePreferenceAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var euCookie = filterContext.HttpContext.Request.Cookies["ConsentCookies"];
if (euCookie == null)
{
// cookie doesn't exist, redirect use to a View showing
//all the cookies being saved in client machine
// and to take user consent(accept or deny)
}
// do something with euCookie
base.OnActionExecuting(filterContext);
}
}
As this rule comes into effect on 25th May 2018, it would be nice to hear your idea regarding different kind of implementation.
Finally, I came up with something that I wanted--intercepting user request and redirecting based upon a certain cookie. This can be used as a nuget as we have multiple applications and saving cookies could be done from one of the application. As it is made as an action filter attribute, it can be place above controller:
[MyAcceptCookieCheck]
public class HomeController : Controller
This makes it easy to implement across all application and operations regarding saving cookies will be done from the one of the application so that it will be easy to make any changes i.e., only from one place.
public class MyAcceptCookieCheck : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var cookies = filterContext.HttpContext.Request.Cookies["OurAcceptCookie"];
var values = filterContext.RouteData.Values.Values;
originalRequest = filterContext.HttpContext.Request.Url.AbsoluteUri;
RouteValueDictionary requestOrigin = new RouteValueDictionary { {
"url", originalRequest } };
if (cookies == null && !values.Contains("CookieConsent")) //so that it won't loop endlessly
{
UrlHelper urlHelper = new UrlHelper(filterContext.RequestContext);
//filterContext.Result = new RedirectResult(urlHelper.Action("CookieConsent", "Home"));
filterContext.Result = new RedirectResult(urlHelper.Action("CookieConsent","Cookie",requestOrigin ,"https","www.my-domain.com/mysitename"));
}
else if(cookies != null)
{
string controllerName = filterContext.RouteData.Values["controller"].ToString();
string actionName = filterContext.RouteData.Values["action"].ToString();
UrlHelper urlHelper = new UrlHelper(filterContext.RequestContext);
filterContext.Result = new RedirectResult(urlHelper.AbsolutePath(actionName, controllerName));
}
}
}
Code for AbsolutePath (courtesy):
public static string AbsolutePath(this UrlHelper url, string actionName, string controllerName, object routeValues = null)
{
string scheme = url.RequestContext.HttpContext.Request.Url.Scheme;
return url.Action(actionName, controllerName, routeValues, scheme);
}
Now, I can redirect all requests without having that particular cookie to a cookie consent page and show user all the details about cookies being used and ask for permission to save "ConsentCookie".
I have overridden the HandleUnauthorizedRequest method in my asp.net mvc application to ensure it sends a 401 response to unauthorized ajax calls instead of redirecting to login page. This works perfectly fine when I run it locally, but my overridden method doesn't get called once I deploy to IIS. The debug point doesn't hit my method at all and straight away gets redirected to the login page.
This is my code:
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.Result = new JsonResult
{
Data = new
{
success = false,
resultMessage = "Errors"
},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.HttpContext.Response.End();
base.HandleUnauthorizedRequest(filterContext);
}
else
{
var url = HttpContext.Current.Request.Url.AbsoluteUri;
url = HttpUtility.UrlEncode(url);
filterContext.Result = new RedirectResult(ConfigurationManager.AppSettings["LoginUrl"] + "?ReturnUrl=" + url);
}
}
}
and I have the attribute [AjaxAuthorize] declared on top of my controller. What could be different once it's deployed to IIS?
Update:
Here's how I'm testing, it's very simple, doesn't even matter whether it's an ajax request or a simple page refresh after the login session has expired -
I deploy the site onto my local IIS
Login to the website, go to the home page - "/Home"
Right click on the "Logout" link, "Open in a new tab" - This ensures that the home page is still open on the current tab while
the session is logged out.
Refresh Home page. Now here, the debug point should hit my overridden HandleUnauthorizedRequest method and go through the
if/else condition and then redirect me to login page. But it
doesn't! it just simply redirects to login page straight away. I'm
thinking it's not even considering my custom authorize attribute.
When I run the site from visual studio however, everything works fine, the control enters the debug point in my overridden method and goes through the if/else condition.
When you deploy your web site to IIS, it will run under IIS integrated mode by default. This is usually the best option. But it also means that the HTTP request/response model isn't completely initialized during the authorization check. I suspect this is causing IsAjaxRequest() to always return false when your application is hosted on IIS.
Also, the default HandleUnauthorizedRequest implementation looks like this:
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs.
filterContext.Result = new HttpUnauthorizedResult();
}
Effectively, by calling base.HandleUnauthorizedRequest(context) you are overwriting the JsonResult instance that you are setting with the default HttpUnauthorizedResult instance.
There is a reason why these are called filters. They are meant for filtering requests that go into a piece of logic, not for actually executing that piece of logic. The handler (ActionResult derived class) is supposed to do the work.
To accomplish this, you need to build a separate handler so the logic that the filter executes waits until after HttpContext is fully initialized.
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new AjaxHandler();
}
}
public class AjaxHandler : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
var httpContext = context.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
if (request.IsAjaxRequest())
{
response.StatusCode = (int)HttpStatusCode.Unauthorized;
this.Data = new
{
success = false,
resultMessage = "Errors"
};
this.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
base.ExecuteResult(context);
}
else
{
var url = request.Url.AbsoluteUri;
url = HttpUtility.UrlEncode(url);
url = ConfigurationManager.AppSettings["LoginUrl"] + "?ReturnUrl=" + url;
var redirectResult = new RedirectResult(url);
redirectResult.ExecuteResult(context);
}
}
}
NOTE: The above code is untested. But this should get you moving in the right direction.
I'm trying to redirect the user to a different action if their email address has not been validated. The thing is, I don't want them to be logged out, I just want to redirect them. When I do this in OnAuthorization for the controller, it redirects as expected, but the user is not authenticated. I'm not sure why this is. My code looks like this:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
//_applicationService.CurrentUser is populated correctly at this point
// from Controller.User
if (_applicationService.CurrentUser != null)
{
if (_applicationService.CurrentUser.EmailVerified != true)
{
var url = new UrlHelper(filterContext.RequestContext);
var verifyEmailUrl = url.Action("EmailVerificationRequired", "Account", null);
filterContext.Result = new RedirectResult(verifyEmailUrl);
}
}
}
Note: I've removed unnecessary code to make it clearer. _applicationService.CurrentUser is populated with the current user - and the user has been authenticated correctly when it gets to that point. But after the redirect the user is no longer authenticated.
How can I achieve this redirect without affecting the built in user authorization?
I've tried putting my code into OnActionExecuting, and I've also tried implementing it in a custom ActionFilterAttribute as well, but wherever I put this redirect in it prevents the 'User' (ie: System.Security.Principal.IPrincipal Controller.User) from getting authenticated.
What am I missing here? Hope this makes sense. Any help much appreciated.
In response to Darin's request for my login action:
[HttpPost]
[AllowAnonymous]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
string errorMessage = "The username or password is incorrect";
if (ModelState.IsValid)
{
if (_contextExecutor.ExecuteContextForModel<LoginContextModel, bool>(new LoginContextModel(){
LoginViewModel = model
}))
{
ViewBag.CurrentUser = _applicationService.CurrentUser;
_formsAuthenticationService.SetAuthCookie(model.LoginEmailAddress, model.RememberMe);
if (_applicationService.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home").Success("Thank you for logging in.");
}
else
{
errorMessage = "Email address not found or invalid password.";
}
}
return View(model).Error(errorMessage);
}
Okay, I've now found where I was going wrong. The problem was that I was being a bit of a goon and I didn't fully understand what was happening when I was doing:
filterContext.Result = new RedirectResult(verifyEmailUrl);
I didn't realise that I am actually starting a new request with this, I mistakenly thought that I was just redirecting to another action. It seems obvious now that this will be a new request.
So, the problem was that my EmailVerificationRequired action was not authorizing the user, and thus when it got to this action the current user was null. So the fix was add the authorization to that action and now it's all fine.
Thanks for your help guys.
You can handle this in your login actionresult. Try placing the
if (_applicationService.CurrentUser.EmailVerified != true)
{
FormsAuthentication.SignOut();
return RedirectToAction("EmailVerificationRequired", "Account");
}
code immediately after this line:
_formsAuthenticationService.SetAuthCookie(model.LoginEmailAddress, model.RememberMe);
Next, set breakpoints and step through the Login action. If you do not reach the if (_applicationService.CurrentUser.EmailVerified != true) line then it indicates that your user is not authenticated, and you have a different problem to address.