3

My team and I are starting up a new website project in ASP .NET 5 and I'm trying to set up the basis of our user authentication and authorization policy.

I am currently toying with cookie-based authentication for returning users, so I've added the following to the Configure method of my Startup.cs:

        // Redirect to login page if user is not logged in.
        app.UseCookieAuthentication(options =>
        {
            options.AutomaticAuthentication = true;
            options.ExpireTimeSpan = new System.TimeSpan(0, 1, 0);
            options.SlidingExpiration = true;
            options.LoginPath = "/Login";
            options.ReturnUrlParameter = "ReturnUrl";
        });

The authentication service is also added to the ConfigureServices method.

My concern is with the LoginPath parameter: while automatic redirection to the Login page works correctly when my middleware/filter (I tried both) returns a 401 code, automatic redirection back from the login page to the one that was originally requested doesn't work: the browser remains on the login page even after a successful login.

I strongly suspect the problem lies somewhere within my controller. Here is its (slightly simplified) source code:

[Route("[controller]")]
[AllowAnonymous]
public class LoginController : Controller
{
    private static readonly string AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;

    public IActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public async System.Threading.Tasks.Task<ActionResult> Post(LoginInfo infos)
    {
        if (ValidateLogin(infos))
        {
            await Context.Authentication.SignInAsync(AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Role, "Admin")
            }, 
            AuthenticationScheme)));
        }

        return View("Index");
    }

    private static bool ValidateLogin(LoginInfo infos)
    {
        return infos.Username == "abc" && infos.Password == "def";
    }
}

(bits of code removed for clarity, e.g. displaying an error message if the username/pwd combination is rejected)

The view is very simple and contains a simple form providing a couple of edit boxes and POST-ing the user/password to the controller (again, error message removed for clarity):

@using (Html.BeginForm("Post", "Login", FormMethod.Post))
{
    <div>
        <h2>Login Page</h2>
        <p>
            @Html.Label("Username")
            @Html.Editor("Username")
        </p>

        <p>
            @Html.Label("Password")
            @Html.Password("Password")
        </p>

        <p>
            <input type="submit" value="Attempt login" />
        </p>
    </div>
}

I've been able to check that user sign-in works correctly by fetching user identity and claims in a separate controller. Initial redirection to the login page also works correctly (i.e. I get redirected to '/Login' when I try to access protected parts of the website without logging in), and the redirected URL correctly contains a ReturnUrl query parameter. I have also tried accessing the Referer URL while handling the Post action and managed to forcefully trigger redirection by manually returning a RedirectResult from the controller, so the contents of the ReturnUrl themselves must be good too.

So... can anyone help me understand why redirection to the return URL is not performed automatically?

edit: according to the documentation of CookieAuthenticationOptions.LoginPath:

Summary: The LoginPath property informs the middleware that it should change an outgoing 401 Unauthorized status code into a 302 redirection onto the given login path. The current url which generated the 401 is added to the LoginPath as a query string parameter named by the ReturnUrlParameter. Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect the browser back to the url which caused the original unauthorized status code. If the LoginPath is null or empty, the middleware will not look for 401 Unauthorized status codes, and it will not redirect automatically when a login occurs.

The part in bold is the one I'm trying to leverage... redirecting to a static page (i.e. manually returning a Redirect() to some hardcoded page) is not what I'm trying to accomplish here.

ZeRemz
  • 1,770
  • 1
  • 13
  • 16
  • when the `ReturnUrlParameter` is empty, how can the framework know where to redirect to? – Khanh TO Aug 12 '15 at 14:22
  • Can you try accessing a protected resource after login to see what happens? – Khanh TO Aug 12 '15 at 14:32
  • Can you try: `HttpContext.GetOwinContext()` instead of `Context`? – Khanh TO Aug 12 '15 at 14:49
  • I'm not sure why `Context` works, but looking at this question, looks like we have to use `HttpContext.GetOwinContext()`: http://stackoverflow.com/questions/26287634/owin-authentication-signin-not-working – Khanh TO Aug 12 '15 at 15:42
  • The actual login works fine: protected resources are unavailable until the controller logs someone in. About the OwinContext, there doesn't seem to be anything like that accessible from my controller. I guess that's one of the many things that changed in ASP 5 / MVC 6. Most code samples and SO questions I've been finding so far refer to classes and/or properties that no longer exist or have been renamed. – ZeRemz Aug 12 '15 at 15:56
  • did you try my updated answer? could it be that the `return View` is executed before the framework trying to redirect the user? – Khanh TO Aug 12 '15 at 15:57
  • I'm trying to get it to compile. The action is expected to return an ActionResult, so I do need to return "something", and I still want to be able to update the view and display a message if the user/pwd is rejected. – ZeRemz Aug 12 '15 at 16:05
  • Make it `Task` instead of `Task`. It's like void return type for async methods. This question also uses `Task`: http://stackoverflow.com/questions/26287634/owin-authentication-signin-not-working – Khanh TO Aug 12 '15 at 16:06
  • Nothing happens if I don't return anything, I end up at /Login with a blank page.. – ZeRemz Aug 12 '15 at 16:21

1 Answers1

0

The problem is you always return to the Index view after a successful login:

[HttpPost]
public async System.Threading.Tasks.Task<ActionResult> Post(LoginInfo infos)
{
        if (ValidateLogin(infos))
        {
            await Context.Authentication.SignInAsync(AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Role, "Admin")
            }, 
            AuthenticationScheme)));
        }

        return View("Index"); //the problem is here
}

Try replacing return View("Index"); with a view that you want to redirect the user to (maybe on another controller). You could try RedirectToAction: Redirect to Action in another controller. Something like:

return RedirectToAction("Index", "Home");

In your case, if you rely on the framework to do redirection for you, try:

[HttpPost]
public async System.Threading.Tasks.Task Post(LoginInfo infos)
{
        if (ValidateLogin(infos))
        {
            await Context.Authentication.SignInAsync(AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Role, "Admin")
            }, 
            AuthenticationScheme)));
        }
 }
Community
  • 1
  • 1
Khanh TO
  • 48,509
  • 13
  • 99
  • 115