Setup a route {tenant}/{controller}/{action}/{id} with ASP.NET MVC?

Joannes Vermorel picture Joannes Vermorel · Nov 9, 2009 · Viewed 7.4k times · Source

I would like to setup a multi-tenant ASP.NET MVC app. Ideally, this app would have a route with {tenant}/{controller}/{action}/{id}, each tenant representing an logical instance of the app (simply independent multi-user accounts)

The fine grained details how do that are still quite unclear to me. Any guide available to setup such multi-tenant scheme with ASP.NET MVC?

Answer

Jeff French picture Jeff French · Mar 9, 2010

I am currently working on a similar project using ASP.Net MVC, Forms Authentication and the SQL providers for Membership/Roles/Profile. Here is the approach I am taking:

  1. Register the default route as `{tenant}/{controller}/{action}/{id}

  2. Change the default behavior of the FormsAuthenticationService that comes with the standard MVC template. It should set the UserData of the authentication ticket to include the tenant name (from your route).

    public void SignIn(string userName, bool createPersistentCookie, string tenantName)
    {
        var ticket = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddMinutes(30),
                                                   createPersistentCookie, tenantName);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
        HttpContext.Current.Response.AppendCookie(cookie);
    }
    
  3. In your global.asax file to do some tenant security checking and allow partioning of users between tenants in one membership database

    protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        //Since this method is called on every request
        //we want to fail as early as possible
        if (!Request.IsAuthenticated) return;
        var route = RouteTable.Routes.GetRouteData(new HttpContextWrapper(Context));
        if (route == null || route.Route.GetType().Name == "IgnoreRouteInternal") return;
        if (!(Context.User.Identity is FormsIdentity)) return;
        //Get the current tenant specified in URL 
        var currentTenant = route.GetRequiredString("tenant");
        //Get the tenant that that the user is logged into
        //from the Forms Authentication Ticket
        var id = (FormsIdentity)Context.User.Identity;
        var userTenant = id.Ticket.UserData;
        if (userTenant.Trim().ToLower() != currentTenant.Trim().ToLower())
        {
            //The user is attempting to access a different tenant
            //than the one they logged into so sign them out
            //an and redirect to the home page of the new tenant
            //where they can sign back in (if they are authorized!)
            FormsAuthentication.SignOut();
            Response.Redirect("/" + currentTenant);
            return;
        }
        //Set the application of the Sql Providers 
        //to the current tenant to support partitioning
        //of users between tenants.
        Membership.ApplicationName = currentTenant;
        Roles.ApplicationName = currentTenant;
        ProfileManager.ApplicationName = currentTenant;
    }
    
  4. Partition each tenants data. Here are two options:

    4a. Use a separate database for each tenant. This provides the best data security for your tenants. In the shared membership database, add a table that is keyed on unique appid for each tenant and use this table to store and retrieve the connection string based on the current tenant.

    4b. Store all data in one database and key each table on the unique tenant id. This provides slightly less data security for your tenants but uses only one SQL Server license.