7

I'm migrating a SaaS app from Classic ASP to .NET MVC5 and will use EF6 Database First. The login form for end users is customisable by each tenant (on their own subdomain but pointing to the same web application). We wish to use the existing database schema and the new authentication & authorization filters.

For example, a user on one tenant may login by entering their first name, surname and a code generated by our system. A user on another tenant may login by entering their email address and a password. Additionally, each tenant has a separate administrator login which uses a username and password. Another tenant may use LDAP authentication against a remote AD server.

Is there a definitive best practice way of doing custom authentication?

Almost every article appears to suggest different ways of accomplishing this: simply setting FormsAuthentication.SetAuthCookie, using a custom OWIN provider, override AuthorizeAttribute, etc.

In Classic ASP, we queried the database to find out the type of login for that tenant, displayed the appropriate fields on the login screen and then on post back, checked the fields match what's in the database and then set the session variables appropriately which were checked on each page request.

Thanks

Marcus
  • 9,011
  • 10
  • 45
  • 65
  • Forms authentication is not the way to go considering Identity and OWIN replace Forms auth in mvc 5+. Overriding Authorize could be the way to go but you haven't described your exact requirements for authentication. – jamesSampica Aug 21 '14 at 05:22
  • Added some more information @Shoe, thanks! – Marcus Aug 21 '14 at 11:33
  • So is the only non-traditional part of your authentication the fact that user admins can build their own means of logging users in? Is there anything special about roles, claims, logins? – jamesSampica Aug 21 '14 at 13:26
  • Admins can choose which fields to authenticate against the database. There are permissions (boolean variables) stored in the user table as columns on which roles/features the user can access. – Marcus Aug 21 '14 at 13:42
  • When admins choose which fields to authenticate, are those fields always present on a User in the Users table? Or can admins choose something like "address" which would be on an Address table connected to a user? – jamesSampica Aug 21 '14 at 16:36
  • The end user login does depend on another table too. Essentially we want to run our own queries against the database and then return true/false (and cache the logged in user details). While it's not a survey app, it's similar to an end user logging in to a tenants survey with a survey code and their surname. – Marcus Aug 21 '14 at 19:38
  • 1
    The best practice method is to user Identity. It has all the framework embedded into it. By you modifying the ViewModel the user fills out to log in, you can specify what they need to login. Here you can check specific values that aren't username and password against your own data context and then use Identity for those that need a username and password. I have done this before and it works wonders. The best approach, IMO, is to use Identity. It's well document, lots of support and it works. Good luck. – Termato Aug 27 '14 at 20:57
  • @Termato thanks. Are both the two answers using Identity? There appears to be subtle differences however both uses CreateIdentityAsync. – Marcus Aug 27 '14 at 22:08
  • I have displayed how I do it in my code as an answer below to better explain anything. It's similar to the other answers but slightly different. Please let me know if you have any questions. – Termato Aug 28 '14 at 15:12
  • Yes, as far as I can tell, both answers are using Identity. I'm using a different Create method than they are though. – Termato Aug 28 '14 at 15:18
  • 2
    I've awarded the bounty to the answer that contained more information but wish I could award bounty too to Shoe as both answers suggest using Identity. – Marcus Aug 28 '14 at 15:22

3 Answers3

7

I find that Identity framework is very flexible in terms of authentication options. Have a look on this bit of authentication code:

var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);

authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);

This is pretty standard run of the mill authentication part in Identity, you'll find this in every Identity sample on the web. If you look closely it is very flexible - all you need for authentication is ApplicationUser object that framework does not care how you get.

So in theory you can do things like this (pseudocode, I did not try to compile this):

// get user object from the database with whatever conditions you like
// this can be AuthCode which was pre-set on the user object in the db-table
// or some other property
var user = dbContext.Users.Where(u => u.Username == "BillyJoe" && u.Tenant == "ExpensiveClient" && u.AuthCode == "654")

// check user for null 

// check if the password is correct - don't have to do that if you are doing
// super-custom auth.
var isCorrectPassword = await userManager.CheckPasswordAsync(user, "enteredPassword");

if (isCorrectPassword)
{
    // password is correct, time to login
    // this creates ClaimsIdentity object from the ApplicationUser object
    var identity = await this.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

    // now we can set claims on the identity. Claims are stored in cookie and available without
    // querying database
    identity.AddClaim(new Claim("MyApp:TenantName", "ExpensiveClient"));
    identity.AddClaim(new Claim("MyApp:LoginType", "AuthCode"));
    identity.AddClaim(new Claim("MyApp:CanViewProducts", "true"));


    // this tells OWIN that it can set auth cookie when it is time to send 
    // a reply back to the client
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Using this authentication, you have set a few claims on the user - they are stored in the cookie and available everywhere via ClaimsPrincipal.Current.Claims. Claims are essentially a collection of key-value pairs of strings and you can store there anything you like.

I usually access claims from the user via extension method:

public static String GetTenantName(this ClaimsPrincipal principal)
{
    var tenantClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:TenantName");
    if (tenantClaim != null)
    {
        return tenantClaim.Value;
    }

    throw new ApplicationException("Tenant name is not set. Can not proceed");
}

public static String CanViewProducts(this ClaimsPrincipal principal)
{
    var productClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:CanViewProducts");
    if (productClaim == null)
    {
        return false;
    }

    return productClaim.Value == "true";
}

So in your controller/view/business layer you can always call to ClaimsPrincipal.Current.GetTenantName() and in this case you'd get "ExpensiveClient" back.

Or if you need to check if a specific feature is enabled for the user, you do

if(ClaimsPrincipal.Current.CanViewProducts())
{
    // display products
}

It is up to you how you store your user properties, but as long as you set them as claims on the cookie, they will be available.

Alternatively you can add claims into the database for every user:

await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));

And this will persist the claim into the database. And by default, Identity framework adds this claim to the user when they login without you needing to add it manually.

But beware, you can't set too many claims on a cookie. Cookies have 4K limit set by browsers. And the way Identity cookie encryption works it increases encoded text by about 1.1, so you can have roughly 3.6K of text representing claims. I've run into this issue here

Update

To control access to controllers via claims you can use the following filter on the controller:

public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
    public string Name { get; private set; }


    public ClaimsAuthorizeAttribute(string name)
    {
        Name = name;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var user = HttpContext.Current.User as ClaimsPrincipal;
        if (user.HasClaim(Name, Name))
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary()
            {
                {"controller", "errors"},
                {"action", "Unauthorised"}
            });
        }
    }
}

and then use this attribute on controllers or separate actions like this:

    [ClaimsAuthorize("Creating Something")]
    public ActionResult CreateSomething()
    {
        return View();
    }

User will require "Create Something" claim on them to access this action, otherwise they will be redirected to "Unauthenticated" page.

Recently I've played with claims authentication and made a prototype application similar to your requirement. Please have a look on the simple version: https://github.com/trailmax/ClaimsAuthorisation/tree/SimpleClaims where claims are stored individually for each user. Or there is more complex solution where claims belong to a role and when users login, role claims assigned to the user: https://github.com/trailmax/ClaimsAuthorisation/tree/master

Community
  • 1
  • 1
trailmax
  • 34,305
  • 22
  • 140
  • 234
  • Claims will be after the authentication process though. This question asks about methods to authenticate in the first place. – jamesSampica Aug 21 '14 at 19:23
  • @Shoe I'm looking at how we handle the whole process so we're able to login the user and then make use of the Authoize attribute and other related things. I am still quite new to .NET MVC. – Marcus Aug 21 '14 at 19:40
  • @Marcus please see my update about restricting access to controllers/action by claims. – trailmax Aug 21 '14 at 20:22
  • Not sure why you'd go through the effort of making a custom authorize when you can just use `Roles` – jamesSampica Aug 21 '14 at 21:15
  • @Shoe Claims are more flexible than roles. In my case I've got to maintain 800+ claims. Having that many roles is crazy. And in fact, in `ClaimsPrincipal` roles are added as claims with `ClaimTypes.Role`. – trailmax Aug 21 '14 at 21:32
  • Yes that's my point. Roles are processed as claims in the identity, and the generic authorize allows for checking of roles. OP has roles which would fit perfectly into the already existing model. – jamesSampica Aug 21 '14 at 21:37
  • @Shoe now that is down to the OP to decide what fits their authentication model. We've used roles for a while and they don't work for us so we have moved to claims with out complex user-defined authentication model. And if you have too many roles on a user, Identity cookie explodes the size and might not be accepted by browser due to 4Kb size limitation. Claims can be not added to the cookie, but on every request, server side. – trailmax Aug 21 '14 at 21:42
  • If you are hitting the cookie limit on claims you are likely misusing claims. – jamesSampica Aug 21 '14 at 21:52
  • @Shoe I am. But I found the way around cookie limitation. Otherwise how would you suggest to have a granular user controller authentication in multitenant application for 800+ MVC actions? – trailmax Aug 21 '14 at 21:55
  • @trailmax: This is a very good answer! Although I would add that Roles and Claims do not map one-to-one. E.g. Say Admins can create/update/delete. So using Role, you have 1 thing to maintain but using claims, you have 3. One can argue on both sides of the fence as to which is better, but in the end it all boils down to what the requirements are and what suits the architecture better. Neither one is a silver bullet. – Mrchief Aug 28 '14 at 17:48
  • @Mrchief absolutely! I find that many developers know about roles, but only a few know about claims, so I stress about the power of claims here. And yes, Claims/Roles can be used together if needed. – trailmax Aug 28 '14 at 17:53
  • Except that mixing them will cause you more grief than anyone on its own! Learnt it from first-hand experience. :) – Mrchief Aug 28 '14 at 17:58
  • @Mrchief Roles are a type of claim. What you are describing are permissions. – jamesSampica Aug 29 '14 at 05:18
2

There's two components you need. The authentication itself and the strategy each user gets for authentication.

The first is easy and is accomplished with these two lines...

var identity = await UserManager.CreateIdentityAsync(user, 
    DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() 
    { IsPersistent = isPersistent }, identity);

When a user is Signed In, they get an identity which contains the user's claims on roles and who they are. These are given to the user as a cookie. After this point you just decorate controllers with [Authorize] to make sure only authenticated users can log in. Pretty standard here.

The only complicated part in the problem is the second part; The strategy for how each user gets authenticated set by the admin.

Some pseudocode for how this could work in actions is this...

// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(int tenantId)
{
    var tenant = DB.GetTenant(tenantId);
    return View(tenant);
}

In your view you would output the authentication strategy for the tenant. That may be email and password, a code and email, or whatever your requirements.

When the user enters their info and clicks to login, you then have to determine what strategy they were using, and check to see if their information matches.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
    var tenant = DB.GetTenant(model.tenantId);
    //If user info matches what is expected for the tenants strategy
    if(AuthenticateUserInfo(tenant, model.UserInputs))
    {
       //Sign the user in
       var identity = await UserManager.CreateIdentityAsync(user, 
           DefaultAuthenticationTypes.ApplicationCookie);
       AuthenticationManager.SignIn(new AuthenticationProperties() 
           { IsPersistent = isPersistent }, identity);
    }
}

I did a lot of hand-waving in the second part because of the complicated nature of how dynamic it is. Overall you should use the same strategies you used in your legacy application to generate the right inputs and such. Nothing has changed there, only the way you sign in is going to be different.

jamesSampica
  • 12,230
  • 3
  • 63
  • 85
  • If I'm understanding correctly, you're suggesting we create a custom OWIN class, return the user similar to the FindAsync method and then use the rest of OWIN code to create the Claims? Am I right in thinking I can use both your answer and @trailmax's together to achieve what I'm looking for? – Marcus Aug 22 '14 at 13:44
  • You don't need a custom OWIN class. You also don't need to create claims. Write a method that validates a user based on what inputs he has entered. After that you just `SignIn`. Claims will be automatically created for the user and contain their roles. – jamesSampica Aug 22 '14 at 13:49
  • Will this support our existing db schema where users may not have a username or email address? – Marcus Aug 22 '14 at 13:51
  • If you're using DB-first you will have to write your own `UserManager`. The UserManager is responsible for creating the claims object. [I wrote an answer on how to do this](http://stackoverflow.com/questions/19940014/asp-net-identity-with-ef-database-first-mvc5/21122865#21122865). – jamesSampica Aug 22 '14 at 14:01
1

Using Visual Studio 2013 Update 3 you can create a new Web Application that comes with MVC5, EF6 and Identity already installed. Here is how to select Identity when you create a new Application:

With MVC Template selected, click Change Authentication and the highlighted window will pop up. Individual User Accounts = Identity. Click ok and continue. Select Identity in MVC Application

Having done that, you have created an application with Identity. You can now customize your login and registration as follows.

You want to look at your AccountController.cs in the Controllers folder. Here you will find the script for Registration and Login.

If you look at the

public async Task<ActionResult> Register(RegisterViewModel model)

function, you'll notice it contains:

IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);

This is where the user gets created. If you want to use Identity, you should save the users username and password. You can use an e-mail as the username if you want. etc.

After doing that, I add the user a specified role (I find the user and then add it to the role):

ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");

In my scenario, I have created an additional extended table where I hold their address, phone number, etc. In that table, you can hold any additional login information. You can add these new entries before or after creating the users account in Identity. I would create the extended information and then create the Identity account just to be sure.

IMPORTANT: For any scenarios where a user is logging in with something that is not a username or e-mail address that isn't saved into via Identity, you will have to do a custom solution.

Example: User types in their first name, surname and the code. You could do two things: Save the first name and surname into the username field of identity and the code into the password and verify the login that way OR you would check your custom table for those properties and make sure they match, if and when they do you could call this little beauty:

await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);

Once you call that SignInAsync function, you can go ahead and direct them to your protected page.

NOTE: I'm creating the ApplicationUser on the function call but if you use it more than once it would be ideal for you to declare the ApplicationUser as follows:

ApplicationUser user = new ApplicationUser() { UserName = model.UserName };

NOTE #2: If you don't want to user Async methods, those functions all have non-async versions of them.

Note #3: At the very top of any page using UserManagement, it is being declared. Make sure if you are creating your own controller that wasn't generated by Visual Studio to use Identity, you include the UserManagement declaration script at the top inside of the class:

namespace NameOfProject.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { }
        public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; }
        public UserManager<ApplicationUser> UserManager { get; private set; }

Please let me know if you have any questions and I hope this helps.

Termato
  • 1,556
  • 1
  • 16
  • 33