I have an MVC app using Forms Authentication, and I'm getting 404 errors. What's happening is the user happens to be submitting a form to a POST-only action when their authentication has timed out, and they are redirected to the login page. After login they are redirected back to the original URL using GET, which will result in a 404 error as the action is POST-only.
I have two questions:
My idea to get around this is to somehow detect whether the action being redirected to is a POST-only action, and redirect to the homepage instead. How would I go about doing that?
Ideally, the app would remember the values posted, and submit them to the original URL via POST, but I have no idea how I would bypass Forms Authentication to do this, and I suspect this would be open to security vulnerabilities. Is this a good idea and if so how could it be done?
A simple fix would be to create a GET only action with the same name as the POST only one, that justs redirects to the homepage. Creating a solution that will resume a form post after a log in would be quite a lot of work for minimal gain.
UPDATE:
Regarding the amount of work it would be to create all of these GET actions.. A more elegant option would be to create an attribute specifically for this scenario, something named like HttpPostOrRedirectAttribute
, that you could use to decorate these post only actions rather than use the HttpPostAttribute
. The behaviour of this would be that it accepts posts, but rather than throwing 404 performs redirects for other verbs.
I created an action filter as a result of the responses above, I'll leave it here for posterity. It passes any specified parameters from the attempted action through to the redirect action.
public class HttpPostOrRedirectAttribute : ActionFilterAttribute
{
public string RedirectAction { get; set; }
public string RedirectController { get; set; }
public string[] ParametersToPassWithRedirect { get; set; }
public HttpPostOrRedirectAttribute(string redirectAction)
: this(redirectAction, null, new string[] { })
{
}
public HttpPostOrRedirectAttribute(string redirectAction, string[] parametersToPassWithRedirect)
: this(redirectAction, null, parametersToPassWithRedirect)
{
}
public HttpPostOrRedirectAttribute(string redirectAction, string redirectController, string[] parametersToPassWithRedirect)
{
RedirectAction = redirectAction;
RedirectController = redirectController;
ParametersToPassWithRedirect = parametersToPassWithRedirect;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.HttpMethod == "POST")
{
base.OnActionExecuting(filterContext);
}
else
{
string redirectUrl = GetRedirectUrl(filterContext.RequestContext);
filterContext.Controller.TempData["Warning"] = "Your action could not be completed as your"
+ " session had expired. Please try again.";
filterContext.Result = new RedirectResult(redirectUrl);
}
}
public string GetRedirectUrl(RequestContext context)
{
RouteValueDictionary routeValues = new RouteValueDictionary();
foreach (string parameter in ParametersToPassWithRedirect)
{
if(context.RouteData.Values.ContainsKey(parameter))
routeValues.Add(parameter, context.RouteData.Values[parameter]);
}
string controller = RedirectController
?? context.RouteData.Values["controller"].ToString();
UrlHelper urlHelper = new UrlHelper(context);
return urlHelper.Action(RedirectAction, controller, routeValues);
}
}
To use, simply replace the HttpPost
filter on the relevant action with HttpPostOrRedirect
thus:
[HttpPostOrRedirect("Display", "User", new[] { "id", "param1", "param2" })]
public ActionResult Delete(User user, int param1, string param2)
{
...
}
Having encountered exactly this problem we create [HttpGet] actions for the Posts that redirected to the Index; the user loses their entered data - so it isn't great - but it was the quick way round it for us.
How about making a GET action that redirects to the page that contains the original form?
You coudl add some extra info to the ModelView data, to indicate that a message should be displayed to the user. This message should say something like this...
"You submitted this form whilst logged out - now that you've logged in, do you wish to continue the submission"
Or, if you really wanted to, the extra information in the ModelViewData could cause the form to be automatically submitted.
As you say, this is quite a bit of extra work, if you have lots of form pages, but there ought to be some way of encapsulating the behaviour for re-use.
Here's improved version of @stusherwin answer with support of ValidateAntiForgeryToken and MVC areas.
Likely your POST Actions have an ValidateAntiForgeryToken attribute to prevent CSRF attacks. In this case ValidateAntiForgeryToken filter will always be executed first since it is Authorization Filter. So we need to make HttpPostOrRedirectAttribute an Authorization Filter as well. Otherwise, exception will be thrown that anti forgery token is not found.
Another improvement is to add redirects to MVC Areas
public class HttpPostOrRedirectAttribute : FilterAttribute, IAuthorizationFilter
{
public string RedirectAction { get; set; }
public string RedirectController { get; set; }
public string RedirectArea { get; set; }
public string[] ParametersToPassWithRedirect { get; set; }
public HttpPostOrRedirectAttribute(string redirectAction)
: this(redirectAction, null, new string[] { })
{
}
public HttpPostOrRedirectAttribute(string redirectAction, string[] parametersToPassWithRedirect)
: this(redirectAction, null, parametersToPassWithRedirect)
{
}
public HttpPostOrRedirectAttribute(string redirectAction, string redirectController, string[] parametersToPassWithRedirect)
{
RedirectAction = redirectAction;
RedirectController = redirectController;
ParametersToPassWithRedirect = parametersToPassWithRedirect;
}
public HttpPostOrRedirectAttribute(string redirectAction, string redirectController, string redirectArea)
{
RedirectAction = redirectAction;
RedirectController = redirectController;
RedirectArea = redirectArea;
}
public HttpPostOrRedirectAttribute(string redirectAction, string redirectController, string redirectArea, string[] parametersToPassWithRedirect)
{
RedirectAction = redirectAction;
RedirectController = redirectController;
RedirectArea = redirectArea;
ParametersToPassWithRedirect = parametersToPassWithRedirect;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.HttpMethod == "POST")
return;
string redirectUrl = GetRedirectUrl(filterContext.RequestContext);
filterContext.Controller.TempData["Warning"] = "Your action could not be completed as your"
+ " session had expired. Please try again.";
filterContext.Result = new RedirectResult(redirectUrl);
}
public string GetRedirectUrl(RequestContext context)
{
RouteValueDictionary routeValues = new RouteValueDictionary();
foreach (string parameter in ParametersToPassWithRedirect)
{
if (context.RouteData.Values.ContainsKey(parameter))
routeValues.Add(parameter, context.RouteData.Values[parameter]);
}
if (RedirectArea.IsNotEmpty())
routeValues.Add("area", RedirectArea);
string controller = RedirectController
?? context.RouteData.Values["controller"].ToString();
UrlHelper urlHelper = new UrlHelper(context);
return urlHelper.Action(RedirectAction, controller, routeValues);
}
}
Here's an example how to use it together with ValidateAntiForgeryToken attribute and redirect to Admin area:
[HttpPostOrRedirect("Display", "User", "Admin", new[] { "id", "param1"}, Order = 0)]
[ValidateAntiForgeryToken(Order = 1)]
public ActionResult Delete(User user, int param1, string param2)
{
...
}