How to redirect to a dynamic login URL in ASP.NET MVC

.Netasp.net MvcAuthenticationForms Authenticationasp.net Routing

.Net Problem Overview


I'm creating a multi-tenancy web site which hosts pages for clients. The first segment of the URL will be a string which identifies the client, defined in Global.asax using the following URL routing scheme:

"{client}/{controller}/{action}/{id}"

This works fine, with URLs such as /foo/Home/Index.

However, when using the [Authorize] attribute, I want to redirect to a login page which also uses the same mapping scheme. So if the client is foo, the login page would be /foo/Account/Login instead of the fixed /Account/Login redirect defined in web.config.

MVC uses an HttpUnauthorizedResult to return a 401 unauthorised status, which I presume causes ASP.NET to redirect to the page defined in web.config.

So does anyone know either how to override the ASP.NET login redirect behaviour? Or would it be better to redirect in MVC by creating a custom authorization attribute?

EDIT - Answer: after some digging into the .Net source, I decided that a custom authentication attribute is the best solution:

public class ClientAuthorizeAttribute: AuthorizeAttribute
{
    public override void OnAuthorization( AuthorizationContext filterContext )
    {
        base.OnAuthorization( filterContext );

        if (filterContext.Cancel && filterContext.Result is HttpUnauthorizedResult )
        {
            filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary
                {
                    { "client", filterContext.RouteData.Values[ "client" ] },
                    { "controller", "Account" },
                    { "action", "Login" },
                    { "ReturnUrl", filterContext.HttpContext.Request.RawUrl }
                });
        }
    }
}

.Net Solutions


Solution 1 - .Net

In the RTM version of ASP.NET MVC, the Cancel property is missing. This code works with ASP.NET MVC RTM:

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Resources;

namespace ePegasus.Web.ActionFilters
{
    public class CustomAuthorize : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
            if (filterContext.Result is HttpUnauthorizedResult)
            {
                filterContext.Result = new RedirectToRouteResult(
                    new System.Web.Routing.RouteValueDictionary
                        {
                                { "langCode", filterContext.RouteData.Values[ "langCode" ] },
                                { "controller", "Account" },
                                { "action", "Login" },
                                { "ReturnUrl", filterContext.HttpContext.Request.RawUrl }
                        });
            }
        }
    }
}

Edit: You may want to disable the default forms authentication loginUrl in web.config - in case somebody forgets you have a custom attribute and uses the built in [Authorize] attribute by mistake.

Modify the value in web.config:

 <forms loginUrl="~/Account/ERROR" timeout="2880" />

Then make an action method 'ERROR' that logs an error and redirects the user to the most generic login page you have.

Solution 2 - .Net

I think the main issue is that if you're going to piggyback on the built-in ASP.NET FormsAuthentication class (and there's no good reason you shouldn't), something at the end of the day is going to call FormsAuthentication.RedirectToLoginPage() which is going to look at the one configured URL. There's only one login URL, ever, and that's just how they designed it.

My stab at the problem (possibly a Rube Goldberg implementation) would be to let it redirect to a single login page at the root shared by all clients, say /account/login. This login page wouldn't actually display anything; it inspects either the ReturnUrl parameter or some value I've got in the session or a cookie that identifies the client and uses that to issue an immediate 302 redirect to the specific /client/account/login page. It's an extra redirect, but likely not noticeable and it lets you use the built in redirection mechanisms.

The other option is to create your own custom attribute as you describe and avoid anything that calls the RedirectToLoginPage() method on the FormsAuthentication class, since you'll be replacing it with your own redirection logic. (You might create your own class that is similar.) Since it's a static class, I'm not aware of any mechanism by which you could just inject your own alternative interface and have it magically work with the existing [Authorize] attribute, which blows, but people have done similar things before.

Hope that helps!

Solution 3 - .Net

My solution to this problem was a custom ActionResult class:

    sealed public class RequiresLoginResult : ActionResult
	{
		override public void ExecuteResult (ControllerContext context)
		{
		    var response = context.HttpContext.Response;

			var url = FormsAuthentication.LoginUrl;
	    	if (!string.IsNullOrWhiteSpace (url))
				url += "?returnUrl=" + HttpUtility.UrlEncode (ReturnUrl);

	    	response.Clear ();
		    response.StatusCode = 302;
		    response.RedirectLocation = url;
	    }

	    public RequiresLoginResult (string returnUrl = null)
	    {
		    ReturnUrl = returnUrl;
	    }

	    string ReturnUrl { get; set; }
    }

Solution 4 - .Net

Still, if one decides to use the built-in ASP.NET FormsAuthentication, one can overide Application_AuthenticateRequest in Global.asax.cs as follows:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
	string url = Request.RawUrl;

	if (url.Contains(("Account/Login"))
	{
		return;
	}

	if (Context.User == null)
	{
		// Your custom tenant-aware logic
		if (url.StartsWith("/foo"))
		{
			// Your custom login page.
			Response.Redirect("/foo/Account/Login");
			Response.End();
			return;
		}
	}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMike ScottView Question on Stackoverflow
Solution 1 - .Netuser134936View Answer on Stackoverflow
Solution 2 - .NetNicholas PiaseckiView Answer on Stackoverflow
Solution 3 - .NetKieronView Answer on Stackoverflow
Solution 4 - .Netturdus-merulaView Answer on Stackoverflow