5

Here is how the scenario goes:

  • Start an MVC project from scratch
  • Test Controller decorated with [Authorize] attribute
  • User Logs in and directed to Home
  • User clicks a link that redirects to the TestController's Index method
  • User waits 60 seconds for the Forms Authentication to timeout
  • User clicks a link that calls an ActionMethod residing on the TestController
  • The MVC framework redirects user to Login page and attaches the ActionMethod name to the URL instead of attaching the Index Action Method

TestController:

[Authorize]
public class TestController : Controller
{
    // GET: Test
    public ViewResult Index()
    {
        return View();
    }

    [ValidateInput(false)]
    public ActionResult ActionTest()
    {
        return new EmptyResult();
    }
}

HomeController:

[Authorize]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

AccountController:

public class AccountController : Controller
{
    [AllowAnonymous]
    public ActionResult Login()
    {
        return View();
    }

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginViewModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            try
            {
                FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);

                if (Url.IsLocalUrl(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                    return RedirectToAction(controllerName: "Home", actionName: "Index");
            }
            catch
            {
                return View(model);
            }
        }
        return View(model);
    }
}

Login.chtml

@model TestLoginProject.Models.LoginViewModel

@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
  .....................
</head>

<body>
    <div class="container">
        @using (@Html.BeginForm("Login", "Account", new { returnUrl = Request.QueryString["ReturnUrl"] }, FormMethod.Post, new { @class = "form-signin" }))
        {
            @Html.AntiForgeryToken()
            ....................
            ....................
        }
    </div>
</body>
</html>

Web Config

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="1" />
</authentication>

The expectation of the return url is:

http://localhost:2441/Account/Login?ReturnUrl=%2fTest%2fIndex

Instead, the current value is:

http://localhost:2441/Account/Login?ReturnUrl=%2fTest%2fActionTest

Notes:

  • When a user clicks the link after timeout, no Test Actions are hit before the redirection to the Login page takes place
  • All routes are the default as provided when starting an Empty MVC project from scratch in VS2017
usefulBee
  • 9,250
  • 10
  • 51
  • 89
  • 1
    share your routing and your view page. Also put a breakpoint in the actions (or log) to see which action is called and in what order if more than one is – Yishai Galatzer Oct 24 '17 at 19:15
  • @YishaiGalatzer, after the Forms Authentication times out, and I attempt to login again, only the SomePagePartialView Action is hit. – usefulBee Oct 24 '17 at 19:24
  • @YishaiGalatzer, the view contains pretty much only the following relevant line: @Html.Action("SomePagePartialView") – usefulBee Oct 24 '17 at 19:27
  • Either something is wrong in your routing table (which I doubt, but you should check), or you login is sending you to the partialview instead of the view (debug that). Last your main view is sending you to the partial view as well with the Action("SomePagePartialView"), for the sake of being 100% sure, make that view just return something else for now (and same with the PartialViewPage) – Yishai Galatzer Oct 25 '17 at 00:54
  • AFAIK there is a workaround to check if `returnUrl` currently requesting view or partial view. If partial view is requested, it should redirect to view page where the partial view is rendered after login (may require overriding `OnActionExecuted` on base controller & use session variable). – Tetsuya Yamamoto Oct 25 '17 at 01:02
  • @usefulBee - Almost every routing question on StackOverflow is like this. People don't seem to realize that the first match always wins, and they don't post the route that actually does match in their question. This behavior means that the order in which your areas vs attribute routes vs convention based routes determines how the URL is built. If you are getting a wrong URL, it is usually because you are assuming MVC is matching a route it is not matching. See [Why map special routes first before common routes in asp.net mvc?](https://stackoverflow.com/a/35674633) – NightOwl888 Oct 25 '17 at 15:00
  • @YishaiGalatzer, that is exactly what happens, "your login is sending you to the partialview instead of the view (debug that)." Also, at the login page the url looks like this, http://localhost:39467/Account/Login?ReturnUrl=%2fMaps%2fSomePagePartialView – usefulBee Oct 25 '17 at 15:02
  • @TetsuyaYamamoto, if OnActionExecuted is to be be overidden, how then will I know what Action method to redirect to, that is associated with the correct View? – usefulBee Oct 25 '17 at 15:18
  • @YishaiGalatzer, just to be accurate, once I hit login, the app hits the ActionResult that returns the PartialView: public virtual ActionResult SomePagePartialView() { return PartialView("_SomePagePartialView"); } – usefulBee Oct 25 '17 at 15:29

1 Answers1

3

This is a normal behavior that you mentioned!

The MVC framework redirects user to Login page and attaches the ActionMethod name to the URL instead of attaching the Index Action Method

Many thanks to MVC Security pipeline. When you use forms authentication and the user is not authenticated or authorized, the ASP.NET security pipeline redirects to the login page and passes returnUrl as a parameter equal to the page that redirected to the login page (here is the controller action which requires authorization which you called by clicking on a link).

So here you can't expect index (currently loaded page with no valid and persistent authentication) and subsequently the ActionMethod calls security pipeline and the returnurl is enumerated just in time.

Note that this is because of Synchronized communication between Controller and View.

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70
  • Thank you for the insight. Unfortunately this "normal behavior" introduces a problem of a different sort since that link might be redirecting to a PartialView and not a View; in this case if a user logins again, he/she will be directed to a distorted or stripped PartialView. What could be the optimal solution to this problem in your opinion? – usefulBee Nov 03 '17 at 20:29
  • @usefulBee, it is not clear what you really want. If the URL is loaded by the browser itself, it should be fully-blown HTML page that browser can show. If the request to URL is done via some AJAX - it is your responsibility to handle response in your Javascript and there you can redirect anywhere you want. I'm not sure what scenario do you mean wheh you mention "_redirecting to a PartialView and not a View_" – SergGr Nov 03 '17 at 22:25
  • @usefulBee You can control the `returnurl` value whether in view (razor) or controller side based on your need to meet the purpose. – Amirhossein Mehrvarzi Nov 04 '17 at 07:21
  • @AmirhosseinMehrvarzi, by PartialView I mean a returnUrl that points to an ActionResult and not a ViewResult. ViewResult is expected to be a fully blown html page with layout etc as you mentioned. – usefulBee Nov 07 '17 at 17:00
  • @usefulBee If you dive into the meaning, you understand when you're working with `PartialView`, you must aware the controller side too. here you can't pass view state to the controller again. Focus on this reality which is *You should notify the controller only, Do you deal with Sync page or Async?*. – Amirhossein Mehrvarzi Nov 07 '17 at 23:35