ASP.NET MVC : Response.Redirect(url, TRUE) does not stop request processing - asp.net-mvc

I have a method decorated with two custom ActionFilterAttribute.
[RequiresAuthentication(Order = 1)]
[ToonAction(Order = 2)]
public ActionResult Browse(...
RequiresAuthentication attribute is coming from this article
Inside RequiresAuthentication, on it's OnActionExecuting I do:
filterContext.HttpContext.Response.Redirect(loginUrl, true);
The line is get executed, and the arguments are all as expected. The problem is that after executing the line above, I get next attribute (ActionFilterAttribute) executed, as if redirect didn't work, it just continues executing the request, instead of simply redirecting browser.
Question: what else do I need to do to make the request handler
This is a complete method:
public override void OnActionExecuting(ActionExecutingContext filterContext) {
//redirect if not authenticated
var identity = filterContext.HttpContext.User.Identity;
if (!identity.IsAuthenticated) {
//use the current url for the redirect
string redirectOnSuccess = filterContext.HttpContext.Request.Url.PathAndQuery;
//send them off to the login page
string redirectUrl = string.Format("?ReturnUrl={0}", redirectOnSuccess);
string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
filterContext.HttpContext.Response.Redirect(loginUrl, true);
// filterContext.Result = new HttpUnauthorizedResult();
// filterContext.HttpContext.Response.StatusCode = 0x191;
}
}

You want to set the Result on the filterContext to a RedirectResult, not do a redirect on the response.
filterContext.Result = new RedirectResult { Url = loginUrl };
EDIT: As #Hunter Daley suggests a better mechanism would be to use the AuthorizeAttribute instead if it works for you. If you do have authentication/authorization scenarios that the AuthorizeAttribute doesn't work for, it would probably be better to derive your custom attribute from it instead of the more generic ActionFilterAttribute. In any event, the correct technique is to set the Result rather than interact with the Response directly. You might want to look at the actual AuthorizeAttribute source at http://www.codeplex.com/aspnet for ideas.
I've got a sample of custom authorization code on my blog, http://farm-fresh-code.blogspot.com, too.

try adding the [Authorize] attribute to your Action methods instead

Add
filterContext.HttpContext.Response.Clear();
at first
and this at End :
filterContext.HttpContext.Response.End();
Hope this helps.

you can use
return RedirectToAction("Index", "Home/Login", new {area = "", returnURL = Request.Url.AbsolutePath});
to stop the current processing, redirect to the desired (login) page and quit the action.
the area route property is needed to get out of the current area if you are in any.

Add this code before you redirect the page.
filterContext.ExceptionHandled = true;

Related

How to capture redirects in ASP.NET MVC

I have an ActionFilter that is successfully capturing page views along with important information from the request. The primary key of this captured entry is then associated with an activity (i.e: Successful Login).
I am trying to now automatically capture Redirects (i.e: RedirectToAction) and associate this with the page view as well. I believe this can be done in my ActionFilter that is capturing page views, but I am unsure if there is a way to tell from the OnActionExecuting context whether or not the GET request is coming from a redirect.
Is there a way to tell from an HttpRequest / ActionExecutingContext(or ActionExecutedContext) whether or not the page is coming from a redirect?
Thanks!
RedirectToAction returns a RedirectToRouteResult
In the OnActionExecuted method of your filter -
if (filterContext.Result is RedirectResult)
{
// It was a RedirectResult
var result = filterContext.Result as RedirectResult;
var url = UrlHelper.GenerateContentUrl(result.Url, filterContext.HttpContext);
}
else if (filterContext.Result is RedirectToRouteResult)
{
// It was a RedirectToRouteResult
var result = filterContext.Result as RedirectToRouteResult;
var url = UrlHelper.GenerateUrl(result.RouteName, null, null, result.RouteValues, RouteTable.Routes, filterContext.RequestContext, false);
}

How to set viewbag in AuthorizedAttribute?

I use MVC 4 and have moved some logic into an authorize filter. I am trying redirect to an error page based on not being authorized. I would like to set the last page route and a few other properties to catch the error.
Below is my override
// handle unauthorized
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Controller.ViewBag.LastRoute = filterContext.RouteData;
filterContext.Controller.ViewBag.Issue = "put...val here";
var routeValues = new RouteValueDictionary(new
{
controller = "Error",
action = "Oops"
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
controller
[AllowAnonymous]
public ActionResult Oops()
{
var m = new Core.Models.ErrorModel();
var v = ViewBag.Issue; // == null
return View("~/Views/Error/Oops.cshtml", m);
}
I tried how to set values to viewbag in actionfilterattribute asp mvc 5 for action filters and it works
Any help would be appreciated.
EDIT:
Sorry when I get to the controller the value for:
ViewBag.Issue = null.
I'm not sure how to set the property and have it hold value.
RedirectToRouteResult is going to send a redirect response to the browser and browser will issue a brand new GET request to the url specified. ViewBag data do not survive between 2 http requests.
You may use TempData which will keep the data between 2 seperate http requests. You can set your TempData value in one action method and any action method that is called after this can get values from the TempData object and then use it. TempData use Session behind the scene to store data. The value of TempData persists until it is read or until the session times out. This is ideal for scenarios such as redirection because the values in TempData are available beyond a single request.
So in your action filter you can set the TempData dictionary instead of ViewBag.
filterContext.Controller.TempData["Issue"] = "Robots are laughing non stop";
var routeValues = new RouteValueDictionary(new
{
controller = "Home",
action = "Oops"
});
filterContext.Result = new RedirectToRouteResult(routeValues);
Now in your Oops action method, you may read the TempData value you set
public ActionResult Oops()
{
var issueDetails = TempData["Issue"];
// TO DO : Do something useful with issueDetails :)
return View();
}
Keep in mind that TempData values won't be available after you read it. So if you want to read it in your view again, set it again or better use a view model and set the already read value as the property value of your view model.

Session is null in RouteHandler

I have spent quite some time going through similar questions here and have not found any that answer my question - apologies if this is a duplicate however I'm pretty sure it's not..
I have an website where the aim is for visitors to complete a form. I am interested in testing different type of forms to ascertain which get filled out more consistently. My idea is that each form has it's own controller and when the user first requests the url it is picked up by a custom route handler which picks 1 form at random and set the relevant controller in RouteData. The chosen formid is then stored in the Session so on subsequnt requests instead of a form being picked at random it will just use the one from the session.
The probem is that I cannot seem to access the Session data in the routehandler - requestContext.Httpcontext.Session is always null. Is this because it is too early in the pipeline? if so how could I achieve this approach?
The first code I tried looked like this:
int FormID = 0;
string FormName = "";
RepositoryManager mgr = new RepositoryManager();
if (requestContext.HttpContext.Session["Form_ID"] != null && requestContext.HttpContext.Session["Form_Name"] != null)
{
int.TryParse(requestContext.HttpContext.Session["Form_ID"].ToString(), out FormID);
FormName = requestContext.HttpContext.Session["Form_Name"].ToString();
}
if (FormID == 0)
{
List<Form> forms = mgr.FormRepository.Get(f => f.FormType.Code == "").ToList();
int rnd = new Random().Next(0, forms.Count - 1);
FormID = forms[rnd].ID;
FormName = forms[rnd].FormName;
requestContext.HttpContext.Session["Form_ID"] = FormID;
requestContext.HttpContext.Session["Form_Name"].ToString();
}
requestContext.RouteData.Values["controller"] = FormName;
return new MvcHandler(requestContext);
This always errored as requestContext.HttpContext.Session is null
I have tried with a custom routehandler then passing off to a custom http handler as follows:
Routehandler
requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
IHttpHandler handler = new FormMvcHandler(requestContext);
return handler;
FormMVCHandler
public class FormMvcHandler : MvcHandler, IRequiresSessionState
{
public FormMvcHandler(RequestContext requestContext)
: base(requestContext)
{
}
protected override void ProcessRequest(HttpContext httpContext)
{
//for testing setting form manually - session will be used here as in original routehandler
RequestContext.RouteData.Values["controller"] = "1Stage";
base.ProcessRequest(httpContext);
}
}
In this second approach changing the controller name has no effect. I have tried changing the controller name in the constructor of the HTTPHandler which does have an effect however If I try and access the session from there using RequestContext.HttpContext.Session it is still null. I have tried setting a breakpoint in ProcessRequest however it is never hit.
Edit 2
This now works by overriding both ProcessRequest(HttpContext httpContext) and BeginProcessRequest(HttpContext httpContext) in the HttpHandler - even when not using an async controller BeginProcessRequest is called by the framework (v3)
In your RouteHandler you have an function GetHttpHandler which return an IHttpHandler. That custom HttpHandler must use IRequiresSessionState and then you can access the Session in the ProcessRequest function in the HttpHandler.
Look into this post:
IRequiresSessionState - how do I use it?
I think you need to use IRequiresSessionState interface
It's too early to using Session in router hander.
you can achieve what you want by using action filter.
Create a Controller named FormController, an action named FormPickerAttribute. In the ActionExecuting of attribute, you can check cookie or session, where your set form id. let's say the form id is "Form1"(create one if null), then you change the action methods to "Form1".
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary{
{ "controller", "Form" },
{ "action", "Form1" },
{ "area", ""}, #your area name
{ "parameter", "parameter value"} #passing any parameter to the action
}
);
you can also create a controller for each form, just updated the
{"controller", "FormIdController"}
to the correct one.

Asp.net Mvc custom mechanism to handle unauthorized request

For my website i want following behaviors for secured controller(or action)
if a user makes a normal request redirect to login page (which i have easily able to do)
if request is Ajax type Request.IsAjaxRequest()==true, return status code 401
How can i create a filter for this??
public class MyCustomAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
//if ajax request set status code and end Response
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
base.HandleUnauthorizedRequest(filterContext);
}
}
Create a filter like above, it will return status code 401 for unauthorized request if request is made thru ajax.
If you are using jQuery you can do as below
jQuery.ajax({
statusCode: {
401: function() {
alert('unauthrized');
},
/*other options*/
});
In addition to the accepted answer, I needed to put this line of code in to prevent FormsAuthentication from redirecting to the login page..
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
I then removed
filterContext.HttpContext.Response.End();
var unauthorizedResult = new JsonResult
{
Data = new ErrorResult() {Success = 0, Error = "Forbidden"},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
// status code
filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
// return data
filterContext.Result = unauthorizedResult;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
Your problem is not with AJAX request, your problem is returning HTTP 401 Unauthorized response, because you use forms authentication. This response code tells the framework that it should redirect the user-agent to your login page with a HTTP 302 response instead. That's why it was easy to setup the "normal" request redirect - it's done automatically.
To answer your question, I had similar problem and the solution I ended up with was not using forms authentication. I implemented a custom authorization attribute that handles both cases manually instead. I'm not sure if this is the best approach, but it does work. I'm interested in what others think of this solution or what other solutions there are.
Fortunately, you can still use the FormsAuthentication class to handle cookies for you, but you have to delete the forms authentication configuration from your Web.config file. When the user logs in you use FormsAuthentication.SetAuthCookie to, well, set a cookie (you are probably doing this already). Second, in your authorization attribute, you get the cookie from the request and use FormsAuthentication.Decrypt to decrypt it. If it exists and is valid, you set the user in the HttpContext based on this cookie, because forms authentication won't do it for you anymore. If it doesn't you either redirect to the login page or return 401, depending on whether it's an AJAX call or not.
You can use ajaxonly to restrain access to ajax actionresult
You can just return a HttpUnauthorizedResult.
Note: This could cause the MVC framework to return you to the login page.
public ActionResult FailResult()
{
return new HttpUnauthorizedResult();
}
a simple way is to do a check in the SignIn action
public ActionResult SignIn()
{
if (Request.IsAjaxRequest())
{
// you could return a partial view that has this script instead
return Content("<script>window.location = '" + Url.Action("SignIn", "Account") + "'</script>");
}
...
return View();

More control on ASP.Net MVC's Authorize; to keep AJAX requests AJAXy

I have some action methods behind an Authorize like:
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(int siteId, Comment comment) {
The problem I have is that I'm sending a request through AJAX to Comment/Create with
X-Requested-With=XMLHttpRequest
which helps identify the request as AJAX. When the user is not logged in and hits the Authorize wall it gets redirected to
/Account/LogOn?ReturnUrl=Comment%2fCreate
which breaks the AJAX workflow. I need to be redirected to
/Account/LogOn?X-Requested-With=XMLHttpRequest
Any ideas how that can be achieved? Any ways to gain more control over what happens when Authorization is requested?
Thanks to Lewis comments I was able to reach this solution (which is far from perfect, posted with my own comments, if you have the fixes feel free to edit and remove this phrase), but it works:
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
override public void OnAuthorization(AuthorizationContext filterContext) {
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest()) {
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
// 2- When leaving Result as a HttpUnauthorizedResult, ASP.Net actually does some nice automatic stuff, like adding a ReturnURL, when hardcodding the URL here, that is lost.
String url = System.Web.Security.FormsAuthentication.LoginUrl + "?X-Requested-With=XMLHttpRequest";
filterContext.Result = new RedirectResult(url);
}
}
}
Recently I ran into exactly the same problem and used the code posted by J. Pablo Fernández
with a modification to account for return URLs. Here it is:
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
override public void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
{
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
HttpRequestBase request = filterContext.HttpContext.Request;
string returnUrl = request.Path;
bool queryStringPresent = request.QueryString.Count > 0;
if (queryStringPresent || request.Form.Count > 0)
returnUrl += '?' + request.QueryString.ToString();
if (queryStringPresent)
returnUrl += '&';
returnUrl += request.Form;
String url = System.Web.Security.FormsAuthentication.LoginUrl +
"?X-Requested-With=XMLHttpRequest&ReturnUrl=" +
HttpUtility.UrlEncode(returnUrl);
filterContext.Result = new RedirectResult(url);
}
}
}
Instead of using the authorize attribute, I've been doing something like the following.
public ActionResult SomeCall(string someData)
{
if (Request.IsAjaxRequest() == false)
{
// TODO: do the intended thing.
}
else
{
// This should only work with AJAX requests, so redirect
// the user to an appropriate location.
return RedirectToAction("Action", "Controller", new { id = ?? });
}
}
I think the right way to handle this would be in your Javascript making the AJAX call.
If the user needs to be authorized (or authenticated as your code implies) and isn't, you should inform them and maybe not allow them to try and comment in the first place.
However, if that doesn't suit your needs.
You could try and write your own authorize action filter, maybe inheriting from the one that comes with the MVC framework but redirects how you want it to. It's fairly straightforward.

Resources