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

asp.net-mvcasp.net-routingauthenticationforms-authenticationnet

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 }
                });
        }
    }
}

Best Answer

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.