Sean Blakemore's Blog

Like trying to fit a square peg in a round hole

20. March 2012 11:44
by Sean
0 Comments

Multiple login pages with ASP.NET MVC

20. March 2012 11:44 by Sean | 0 Comments

I’m working on a multi-tenant web application where I need to have several different login forms for different types of users. For example for Admins, Affiliates, Customers etc.

The problem

This causes a problem because the Forms Authentication bits assume there will be a single login page for the whole application and it’s only possible to set a single loginUrl in the web.config to redirect users requesting pages which require authentication.

The solution

There are a couple of solutions floating around but most of them feel really dirty. You also can’t get away with putting another authentication element in a web.config file in a subfolder of your site because:

It is an error to use a section registered as allowDefinition='MachineToApplication' beyond application level.

In my case things are slightly more complicated because I will be composing my application from modules which are loaded in at runtime. These modules can also add their own login pages so I need something extensible.

No, really - the solution

We can’t fight that ASP.NET will only support redirecting to a single location, so let us instead redirect to a location whose ‘single responsibility’ is to forward the user to the login page they actually need. Enter the LoginRedirectController!

public class LoginRedirectController : Controller
{
    public ActionResult Index(string returnUrl)
    {
        //TODO: Redirect!
    }
}

And then we need to register this in the web.config as our loginUrl.

<authentication mode="Forms">
  <forms loginUrl="~/LoginRedirect" requireSSL="true" cookieless="UseCookies" />
</authentication>

We’re going to be looking at the returnUrl parameter which ASP.NET populates for us to figure out where to send people from here. I’m breaking up these different sections of the application into MVC Areas and the default behaviour is to have a prefix to the URL for every page in a given area. For example the Admin area has a route which looks like this: Admin/{controller}/{action}/{id}. Fleshing out our LoginRedirectController we arrive at:

private readonly IReturnUrlHandler returnUrlHandler;

public LoginRedirectController(IReturnUrlHandler returnUrlHandler)
{
    this.returnUrlHandler = returnUrlHandler;
}

public ActionResult Index(string returnUrl)
{
    var route = returnUrlHandler.GetRoute(returnUrl);
    return RedirectToRoute(route, new { returnUrl });
}

Nice and simple! We have a (badly named) interface called IReturnUrlHandler and a GetRoute method to which we can pass the returnUrl. The job of the IReturnUrlHandler is simply to map between a returnUrl and a named route. We make sure to pass the returnUrl on to whatever route we’re forwarding to so the user will eventually arrive at whatever page they originally requested after being logged in.

public interface IReturnUrlHandler
{
    void Register(string returnUrlPrefix, string loginRoute);
    string GetRoute(string returnUrl);
}

public class ReturnUrlHandler : IReturnUrlHandler
{
    private const string DefaultRoute = "Home";
    private readonly Dictionary<string, string> redirectRules = new Dictionary<string, string>();

    public void Register(string returnUrlPrefix, string loginRoute)
    {
        redirectRules.Add(returnUrlPrefix, loginRoute);
    }

    public string GetRoute(string returnUrl)
    {
        var key = redirectRules.Keys.FirstOrDefault(x =>
            returnUrl.StartsWith(x, StringComparison.OrdinalIgnoreCase));
        return !string.IsNullOrEmpty(key) ? redirectRules[key] : DefaultRoute;
    }
}

No magic in here at all. We have a register method which allows us to add prefix and route combinations to a dictionary which is then used to resolve the name of a suitable route in our LoginRedirectController. Note that if we don’t find a matching route we send them to one named “Home” which just dumps them back on the root of the site. To use this final piece we just need to add named routes to our different login pages and register them.

context.MapRoute("Admin_Login", "Admin/Login", 
                new { controller = "Auth", action = "Login" });

returnUrlHandler.Register("/Admin", "Admin_Login");

And finally just to complete the picture we have our AuthController in the Admin area.

[HttpGet]
public ActionResult Login()
{
    if (Request.IsAuthenticated)
        return RedirectToAction("Index", "Dashboard");

    return View(new AdminLoginModel());
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(AdminLoginModel input, string returnUrl)
{
    //TODO: Validate username and password against something

    if (ModelState.IsValid)
    {
        formsAuth.SetAuthCookie(input.Username, false);

        //Important! Check IsLocalUrl to prevent open redirection attack!
        if (Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);
        return RedirectToAction("Index", "Dashboard");
    }

    return View(new AdminLoginModel { Username = input.Username });
}

Summary

It certainly does the job and is very clean. Perfect for me because my modules can be loaded in at runtime and register their own login pages, it’s up to you whether your situation is something where the benefits of this outweigh the ceremony involved.

Pingbacks and trackbacks (1)+

Comments are closed