0

I've changed the web.config file adding the following lines.

<system.web>
  <authorization>
    <deny users="?"/>
  </authorization>
  <authentication mode="Forms">
    <forms loginUrl="~/Account/LogIn"></forms>
  </authentication>
</system.web>

Now, whatever page's accessed, I'm being redirected to the login page. That's great and as I enter the credentials, I'd like the user to be directed back to where they started.

public ActionResult LogIn(string token)
{
  using (Model model = new Model())
    if (model.Users.Any(_ => _.Token == token))
      return Redirect(Request.UrlReferrer.ToString());
  return View();
}

The problem I discover is that the UrlReferrer is the login page itself so I'm only redirecting to where I'm already at on the log in page, instead of where I originally started...

What am I doing wrong?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438

2 Answers2

2

What am I doing wrong?

You are using the old ASP.NET security (which is based on physical files and folders) for MVC (which is based on controllers and actions). The proper way to secure controllers and actions is to use AuthorizeAttribute. ASP.NET security won't work for MVC controllers because they are not file-system based (but do note it does still come in handy for physical files - you can block direct access to them and then use a controller to provide conditional access).

When you use AuthorizeAttribute, the login URL will automatically be built with a ReturnUrl query string parameter which is used to redirect the user back to the location they started at.

For example, in the default MVC templates, this is the Login method which uses the returnUrl. It also uses RedirectToLocal to ensure someone doesn't exploit the query string parameter to hijack the user to another website.

MVC 4 (Simple Membership):

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
        {
            return RedirectToLocal(returnUrl);
        }

        // If we got this far, something failed, redisplay form
        ModelState.AddModelError("", "The user name or password provided is incorrect.");
        return View(model);
    }

MVC 5 (Identity):

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, change to shouldLockout: true
        var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
        }
    }
Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Give me some lingo to google for, then, please. I've followed a guide or two but must have missed the time issue. – Konrad Viltersten Apr 03 '16 at 19:40
  • Oh, wait - so I'm supposed to authority declare **every** action? I was hoping for a configuration saying that *all's protected except this or that*... – Konrad Viltersten Apr 03 '16 at 19:41
  • 1
    You can register `AuthorizeAttribute` as a [global filter](http://odetocode.com/blogs/scott/archive/2011/01/18/configurable-global-action-filters-for-asp-net-mvc.aspx), and then use the `AllowAnonymous` attribute to selectively allow actions to be accessed. This is the recommended way to do it. Also see: https://msdn.microsoft.com/en-us/library/gg416513(VS.98).aspx – NightOwl888 Apr 03 '16 at 19:44
  • Awesome info. Just a question - I can see the *Request.UrlReferrer.OriginalString* and the last part of it is the query string I want to redirect to. How do I access that? I can do some string logics but I'm sure there's a simple way to pick out where you're supposed to be redirected. – Konrad Viltersten Apr 03 '16 at 19:54
  • I posted the `Login` method from the default MVC 4 template so you can see how it is done. Generally, referrer is not as reliable because it depends on whether the browser supplies one (no guarantees), so it is better if you use the query string parameter `returnUrl`. – NightOwl888 Apr 03 '16 at 20:59
1

Pass Through

You need to pass through the original page through your login process, assuming you want to use the URLReferrer it might look something like this.

[HttpGet]
public ActionResult LogIn()
{
  Viewbag.ReturnURL = Request.UrlReferrer.ToString();
  return View();
}

[HttpPost]
public ActionResult LogIn(string token, string returnURL)
{
  using (Model model = new Model())
    if (model.Users.Any(_ => _.Token == token))
      return Redirect(returnURL);
  return View();
}

Warning: Using the UrlReferrer property can be error prone because it relies on information that may be unpredictable depending on the circumstances in which the request occurred.

42shadow42
  • 405
  • 2
  • 9