Question

We are currently developing an ASP.NET MVC 5.1 application for deployment to customer sites where we will not control their IIS configuration. The application uses ASP.NET Identity 2.0.0 for authentication and user management. During internal testing we have different instances in virtual directories (as separate applications and under separate application pools) on our test server running Windows Server 2012 R2 with IIS 8.

I have had a bug reported that attempting to login with a URL like -

https://server.domain.com/VirtualDirectory/Account/Login?ReturnUrl=%2fVirtualDirectory

Results in a loop whereby the user is logged in but is redirected back to the login page instead of the application root/home page. The obvious standout point here is the missing trailing slash from the virtual directory name. If an encoded trailing slash is provided, or the returnUrl is omitted or not a local URL, then the application correctly redirects successfully.

This is not an issue with our login logic as a user who is already logged in and at the root of the application will be redirected back to the login page simply by removing the trailing slash after the virtual directory name to leave-

https://server.domain.com/VirtualDirectory

According to IIS generates courtesy redirect when folder without trailing slash is requested-

"When a browser requests a URL such as http://www.servername.de/SubDir, the browser is redirected to http://www.servername.de/SubDir/. A trailing slash is included at the end of the URL... Internet Information Server (IIS) first treats SubDir as a file that it should give back to the browser. If this file cannot be found, IIS checks to see if there is a directory with this name. If a directory with this name exists, a courtesy redirect with a 302 "Object moved" response message is returned to the browser. This message also contains the information about the new location of the directory with the trailing slash. In turn, the browser starts a new GET request to the URL with the trailing slash."

I am in fact though getting the following response from Fiddler-

GET https://server.domain.com/VirtualDirectory

302 Redirect to /VirtualDirectory/Account/Login?ReturnUrl=%2fVirtualDirectory

The default route is unmodified from the Microsoft templates-

 routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );

The home controller is decorated with an [Authorize] attribute which would of course require an unauthenticated user to login.

The Web.config settings pertinent to authentication are-

<authentication mode="None">
   <forms cookieless="UseCookies" loginUrl="~/Account/Login" name="OurCompanyAuthentication" timeout="120" />
</authentication>
<authorization>
   <allow users="?" />
   <allow users="*" />
</authorization>

The forms element has proven necessary with experimentation to correctly map to the login page and prevent inherited configuration looking for a login.aspx page at the root of the application. The configuration matches that of Startup.Auth.cs which is-

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    CookieHttpOnly = true, //In supported browsers prevent JavaScript from accessing the authentication cookie
    CookieName = "OurCompanyAuthentication",
    CookiePath = VirtualPathUtility.ToAbsolute("~/"),
    ExpireTimeSpan = new TimeSpan(hours: 2, minutes: 0, seconds: 0), //Cookie expires two hours after it is issued
    LoginPath = new PathString("/Account/Login"),
    SlidingExpiration = true //Cookie will be re-issued on requests more than halfway through expiration window
});

The question-

What is unconditionally generating the 302 redirect to the login page with the returnUrl unmodified/uncorrect (this happens regardless of if the user was already logged in), and how can I restore the described behaviour of performing a 302 redirect with the slash appended.

If I can conditionally direct to the login page with a correct return URL then so much the better, but the primary requirement is avoiding the incorrect redirect to the login page and subsequent loop in the first place. If the 302 redirect to the application root is then followed by a further redirect to the login page (where the user is not authenticated or their ticket has expired) then that is acceptable.

I've looked into URL rewriting but the domain and virtual path are not known to developers in advance as these may be different on each customer site - as is the use or otherwise of virtual directories on the host server.

Was it helpful?

Solution

I think this code should achieve what I wanted both in terms of fixing the missing slash and redirecting to the login page if the user isn't already authenticated to save a wasted redirect-

protected void Application_ResolveRequestCache(object sender, EventArgs e)
{
    //If the application is installed in a Virtual Directory and the trailing slash is ommitted then permantently redirect to the default action
    //To avoid wasted redirect do this conditional on the authentication status of the user - redirecting to the login page for unauthenticated users
    if ((VirtualPathUtility.ToAbsolute("~/") != Request.ApplicationPath) && (Request.ApplicationPath == Request.Path))
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            var redirectPath = VirtualPathUtility.AppendTrailingSlash(Request.Path);

            Response.RedirectPermanent(redirectPath);
        }

        var loginPagePath = VirtualPathUtility.ToAbsolute("~/Account/Login");

        Response.StatusCode = 401;
        Response.Redirect(loginPagePath);
    }
}

As I have set the cookie path to be the application directory though, the user cookie is not being sent when the request is missing the trailing slash, so the user can never be authenticated. As a result I have moved to an event earlier in the request lifecycle and simplified to-

protected void Application_BeginRequest(object sender, EventArgs e)
{
    //If the application is installed in a Virtual Directory and the trailing slash is ommitted then permantently redirect to the default action
    //To avoid wasted redirect do this conditional on the authentication status of the user - redirecting to the login page for unauthenticated users
    if ((VirtualPathUtility.ToAbsolute("~/") != Request.ApplicationPath) && (Request.ApplicationPath == Request.Path))
    {
        var redirectPath = VirtualPathUtility.AppendTrailingSlash(Request.Path);

        Response.RedirectPermanent(redirectPath);

        return;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top