aspnet / Mvc

[Archived] ASP.NET Core MVC is a model view controller framework for building dynamic web sites with clean separation of concerns, including the merged MVC, Web API, and Web Pages w/ Razor. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
5.61k stars 2.14k forks source link

ASP.NET MVC Core - returnurl is always null #5242

Closed saf-itpro closed 8 years ago

saf-itpro commented 8 years ago

In my ASP.NET MVC Core web app with Individual User Account authentication, the returnurl in the default login() get and post action methods (shown below) is always null. I verified it by placing a breakpoint after the line ViewData["ReturnUrl"] = returnUrl;. When I login to the app I see at the breakpoint that returnUrl is always null. Why is that even though I can see that the corresponding form tag has asp-route-returnurl="@ViewData["ReturnUrl"]" attribute value defined? I see the same null behavior in other action methods where I'm using returnUrl and making sure that the corresponding form tags of these action methods have asp-route-returnurl="@ViewData["ReturnUrl"]" attribute defined.

AccountController:

[Authorize]
public class AccountController : Controller
{
    // GET: /Account/Login
    [HttpGet]
    [AllowAnonymous]
    public IActionResult Login(string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        return View();
    }
    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        if (ModelState.IsValid)
        {
            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, set lockoutOnFailure: true
            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                _logger.LogInformation(1, "User logged in.");
                return RedirectToLocal(returnUrl);
            }
            if (result.RequiresTwoFactor)
            {
                return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning(2, "User account locked out.");
                return View("Lockout");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                return View(model);
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }
}

login.cshtml view: <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal"> .....//rest of the html removed for brevity ..... </form>

rynowak commented 8 years ago

What's the output of the form tag look like in the generated HTML? - is the returnurl part of the query string for the form?

saf-itpro commented 8 years ago

@rynowak The generated HTML for form tag is shown below. The app logs me in correctly and shows Hello MyLoginName and logoff links at the top right corner of the page after I successfully log in.

<form method="post" id="logoutForm" class="navbar-right" action="/Account/LogOff">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a title="Manage" href="/Manage">Hello myLoginName@myDomain.com!</a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
            </li>
        </ul>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Mi8s83lkBNHlpnJILYJXeOWmopD7NdTI4hAqL49NZIbrM-Ouj7T6ciNW4As8kh34GIIeGXqHvVq4YmPYpan_cD28fXXDTRmGqxTGqYAreAh3U_5rZtyd3ZSv_YXG-zgA3vp_QdtLY1K47G6wCSaX9piRMQrnoDOMGxQu6-7bAbX5D0LGvuEESH8eufrk3h0kw" />
</form>
rynowak commented 8 years ago

@dougbu @NTaylorMullen - something funky is going on here with the taghelper generating the URL.

dougbu commented 8 years ago

@saf-itpro by default, RequiresTwoFactor is false for the Individual User Account scheme. Have you changed that in your project? If not, returnUrl is only ever set if it's model bound and I don't see the <form> submitting such a value.

If I haven't hit on the problem, please provide a full repro project. Ideally upload something small to a public GitHub repo.

saf-itpro commented 8 years ago

@dougbu Let me check what you are suggesting. But to note: I just created a project in VS2015-Update3 using a default template ASP.NET Core Web Application (.NET Core) and choosing the authentication to be Individial User Accounts. And, noticed this behavior on Get and Post action methods named Login(..) and Register(). I did not change anything in the test project that I created. One can test this behavior by creating a test project as I described above.

saf-itpro commented 8 years ago

@dougbu I did not see an option to change RequiresTwoFactor value. I do not know how to upload the project in public GitHub repo. But following are the steps I used to create the project and still the same returnurl issue: This seems to be a bug or please let me know reason why returnurl returns null value. Part of AccountController.cs and login.cshtml view are given in the original post above.

  1. In VS2015-Update3, created a new project using ASP.NET Core Web Application (.NET Core) project template and choosing Individual User Accounts authentication.
  2. Ran the command in Package Manage console: PM> update-database -context ApplicationDbContext. Note: This command creates necessary ASP.NET Identity tables ASPNETUsers, etc in LocalDb
  3. Successfully compiled the app and ran the app in debug mode and performed the test as follows:
    • Placed a breakpoint in Login(...) and Register(...) Get/Post action methods.
    • Clicked on Register link on the upper right corner of the home page and noticed that in the Register(...) get method the returnurl was null. After entering the new username password info clicked on Register button and noticed that the returnurl variable was still null in the post Register(...) method.
  4. After successfully registering the username/password, clicked on the login link in the upper right corner of the home page and repeated the same test [as for Register(....) action methods above] for Login(...) Get/Post action methods. The reutrnrul variable was null there as well.
javiercn commented 8 years ago

The return url is null because you are trying to log in / register directly. Try to access a protected resource (an action with [Authorize] on it and you'll see that the return url points to the original resource you were trying to access to.

dougbu commented 8 years ago

@saf-itpro did @javiercn's comment help you solve the problem? I won't actively investigate 'til I hear back.

saf-itpro commented 8 years ago

@javiercn I tried your suggestion as shown below. But still returnUrl is null. Steps are as follows:

  1. Created this exact web app from official ASP.NET Website EXCEPT that I chose Individual User Accounts authentication
  2. RanPM> update-database -context ApplicationDbContext to create ASP.NET Identity tables in LocalDb.
  3. Added asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" attributes to the form element of Create.cshtml file
  4. Created a link on the Home page as: <a asp-controller="Movies" asp-action="Create">Test</a>
  5. Added [Authorize] on top of the MoviesController shown below. Added string returnUrl = null as input parameters in Create(...) Get/Post methods. Added ViewData["ReturnUrl"] = returnUrl; in those methods. Placed a breakpoint inside Create(..) Get/Post methods.
  6. Ran the app in Debug Mode. Successfully registered and logged in. Clicked on the link of step3 above.
  7. Observed in debug mode that the returnUrl in Create(...) (both Get and Post methods) was still null. However the app ran successfully and I even created some test movies.
[Authorize]
    public class MoviesController : Controller
    {
        private readonly ApplicationDbContext _context;

        public MoviesController(ApplicationDbContext context)
        {
            _context = context;    
        }

        // GET: Movies/Create
        public IActionResult Create(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        // POST: Movies/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("ID,Genre,Price,ReleaseDate,Title")] Movie movie, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                _context.Add(movie);
                await _context.SaveChangesAsync();
                return RedirectToAction("Index");
            }
            return View(movie);
        }
}

View Source of the form element of Create.cshtml:

<form method="post" action="/Movies/Create">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger"></div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="Genre">Genre</label>
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true" />
            </div>
        </div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="Price">Price</label>
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true" />
            </div>
        </div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
            <div class="col-md-10">
                <input class="form-control" type="datetime" data-val="true" data-val-required="The ReleaseDate field is required." id="ReleaseDate" name="ReleaseDate" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true" />
            </div>
        </div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="Title">Title</label>
            <div class="col-md-10">
                <input class="form-control" type="text" id="Title" name="Title" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true" />
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8Mi8s83lkBNHlpnJILYJXeMZZmICDdXy6j7XThwq9heJ2hCsR3CkoRZdOw_5EdkkFfOX0EGa-qwP1crbZA-itgKjUrYvGt3nsMkMB-4GttfahG27OKEtSX1s2jyEK2KAH8Lp-4xfNbnQ9g90pMM2dgkv_rmpClnnQYZ5KMT0VJgkwLPwngve5_QBVyyQVXj3sA" /></form>
saf-itpro commented 8 years ago

@dougbu I'm still getting returnUrl as null. The test process (suggested by @javiercn ) is described above.

dougbu commented 8 years ago

Hmm, thought I commented on this issue a while ago.

I cannot reproduce the behaviour you describe. If I'm currently logged out and navigate to a page requiring a logged-in user, returnUrl is never null. That is returnUrl works when it's supposed to work.

Note there's no reason to change the created individual accounts project e.g.

  1. Start the new site
  2. Register
  3. Log out
  4. Enter [http://localhost:5000/Manage]() in your browser

Note you're redirected to [http://localhost:5000/Account/Login?ReturnUrl=%2FManage]()

saf-itpro commented 8 years ago

@dougbu So, once you logged in returnUrl will always be null. In an ASP.NET MVC project, when you decorate a class or method with [Authorize] and authorization fails, the site automatically redirects to the login page (using the loginUrl specified in web.config). In addition, something in the ASP.NET MVC framework passes along the original request's URL as a ReturnUrl parameter.

akberc commented 7 years ago

FWIW: I had the exact same symptom when moving from a Windows development machine to Linux test machine, with a copied database that did not have ASP identity tables. At first, I thought it was case sensitivity, however, once the tables were restored, everything worked exactly as it did on the development machine.