Tuesday, December 15, 2015

ASP.Net 5 Identity and Authentication - Customize with Your Own User Tables

Microsoft has an excellent Identity class to handle user authentication. You can simply apply the security pattern and avoid complex page and business logic inside. ASP.Net 5 included the pattern within their new ASP.Net 5 project template. The only issue is for legacy system, we usually have our own user tables, and in most cases we don't want to give up them.  So customize Microsoft Identity pattern with our own user database would be very import and useful.

First off, we need to understand ASP.Net Core Identity.
IUser
A user object must implement the IUser interface, which requires to have at least an Id and a UserName. Note Id property has type string, which can handle GUIDs.
public interface IUser
{
   string Id { get; }
   string UserName { get; set; }
}
IUserStore
IUserStore defines the basic CRUD functionality for users. If you want to create local accounts with passwords, you need to implement IUserPasswordStore to save user and password. For third party logins (Twitter and Facebook, for example), add IUserLoginStore, and for claim storage there is an IUserClaimStore. Note IUserStore has a generic constraint.
public interface IUserStore<TUser> : IDisposable where TUser : IUser
{
   Task CreateAsync(TUser user);
   Task DeleteAsync(TUser user);
   Task<TUser> FindByIdAsync(string userId);
   Task<TUser> FindByNameAsync(string userName);
   Task UpdateAsync(TUser user);
}
IUserClaimStore & IUserRoleStore & IUserPasswordStore
Microsoft Identity supports Claims through interface IUserClaimStore.
public interface IUserClaimStore<TUser> 
   : IUserStore<TUser>, IDisposable where TUser : IUser
{
   Task AddClaimAsync(TUser user, Claim claim);
   Task<System.Collections.Generic.IList<Claim>> GetClaimsAsync(TUser user);
   Task RemoveClaimAsync(TUser user, Claim claim);
}
It seemed Microsoft still want to keep User Role along with User Claims. Both of them are optional.
public interface IUserRoleStore<TUser> 
   : IUserStore<TUser>, IDisposable where TUser : IUser
{
   Task AddToRoleAsync(TUser user, string role);
   Task<System.Collections.Generic.IList<string>> GetRolesAsync(TUser user);
   Task<bool> IsInRoleAsync(TUser user, string role);
   Task RemoveFromRoleAsync(TUser user, string role);
}
For user account, password are also optional. However, in most cases, you need to implement this interface as you want your users to use their passwords to login. In addition, you can use this to persist passwords (actually, hashed passwords).
public interface IUserPasswordStore<TUser> 
   : IUserStore<TUser>, IDisposable where TUser : IUser
{
   Task<string> GetPasswordHashAsync(TUser user);
   Task<bool> HasPasswordAsync(TUser user);
   Task SetPasswordHashAsync(TUser user, string passwordHash); 
}
UserManager & AccountController
The UserManager is a concrete class to handle identity and membership business logic, such as hash a password, validate a user, and manage claims.


UserManager has a number of properties you can use. For example,  a custom UserValidator (any object implementing IIdentityValidator), a custom PasswordValidator, and a custom
PasswordHasher.

You can pass any object which implementing IUserStore via the UserManager constructor. You use UserManager to manage user information stored in SQL Server, a document database, or other forms of storage.

UserManager will be created within AccountController by passing in a new UserStore, which implements IUserPasswordStore in addition to the other core identity interfaces for persisting claims, roles, and 3rd party logins.

Note The WebAPI and Single Page Application project templates also support user registration and password logins, but in these templates the AccountController is an API controller that issues authentication tokens instead of authentication cookies.




public class UserManager<TUser> : IDisposable where TUser : IUser
{
   public UserManager(IUserStore<TUser> store);
   public ClaimsIdentityFactory<TUser> ClaimsIdentityFactory { get; set; }
   public IPasswordHasher PasswordHasher { get; set; }
   public IIdentityValidator<string> PasswordValidator { get; set; }
   protected IUserStore<TUser> Store { get; }
   public virtual bool SupportsUserClaim { get; }
   public virtual bool SupportsUserLogin { get; }
   public virtual bool SupportsUserPassword { get; }
   public virtual bool SupportsUserRole { get; }
   public virtual bool SupportsUserSecurityStamp { get; }
   public IIdentityValidator<TUser> UserValidator { get; set; }
 
   public virtual Task<IdentityResult> AddClaimAsync(string userId, Claim claim);4
   public virtual Task<IdentityResult> AddLoginAsync(string userId, UserLoginInfo login);
   public virtual Task<IdentityResult> AddPasswordAsync(string userId, string password);
   public virtual Task<IdentityResult> AddToRoleAsync(string userId, string role);
   public virtual Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword);
   public virtual Task<IdentityResult> CreateAsync(TUser user);
   public virtual Task<IdentityResult> CreateAsync(TUser user, string password);
   public virtual Task<ClaimsIdentity> CreateIdentityAsync(TUser user, string authenticationType);
   public virtual Task<TUser> FindAsync(UserLoginInfo login);
   public virtual Task<TUser> FindAsync(string userName, string password);
   public virtual Task<TUser> FindByIdAsync(string userId);
   public virtual Task<TUser> FindByNameAsync(string userName);
   public virtual Task<Collections.Generic.IList<Claim>> GetClaimsAsync(string userId);
   public virtual Task<Collections.Generic.IList<UserLoginInfo>> GetLoginsAsync(string userId);
   public virtual Task<Collections.Generic.IList<string>> GetRolesAsync(string userId);
   public virtual Task<bool> HasPasswordAsync(string userId);
   public virtual Task<bool> IsInRoleAsync(string userId, string role);
   public virtual Task<IdentityResult> RemoveClaimAsync(string userId, Claim claim);
   public virtual Task<IdentityResult> RemoveFromRoleAsync(string userId, string role);
   public virtual Task<IdentityResult> RemoveLoginAsync(string userId, UserLoginInfo login);
   public virtual Task<IdentityResult> RemovePasswordAsync(string userId);
   public virtual Task<IdentityResult> UpdateAsync(TUser user);
   public virtual Task<IdentityResult> UpdateSecurityStampAsync(string userId);
}
Creating UserManager in AccountController:
new UserManager<ApplicationUser>(
    new UserStore<ApplicationUser>(new ApplicationDbContext()))
ApplicationDbContext & IdentityDbContext
UserStore has a dependency on an Entity Framework class named ApplicationDbContext which derives from IdentityDbContext
ApplicationDbContext provides all of the Entity Framework code-first mapping.  If we want to use our own user tables, we need to change ApplicationDBContext or use our own to replace it.
By default, IdentityDbContext uses a connection string named “DefaultConnection”, and all the new project templates will include a DefaultConnection connection string in the project’s web.config file. The connection string points to a SQL Local DB database in the AppData folder.

To change the SQL Server database being used, you can change the value of the connection string in web.config. You can also pass a different connection string name into the DB context constructor.

ApplicationUser & UserStore
The ApplicationUser and UserStore are class implemented IUser and IUserStore using Entity Framework 6.  ApplicationUser includes Id, Username, PasswordHash, and other properties it inherits from IdentityUser.
public class ApplicationUser : IdentityUser {
}
 
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> {
    public ApplicationDbContext() : base("DefaultConnection") {
    }
}
Customize Microsoft Identity Framework
Then we can start to implement our own User class, and it should extends IUser interface. Actually we can mimic ApplicationUser class. Under Entity Framework, the User class would come from directly our database. We can change the generated code, which is actually bad. This is just offer one solution for customization.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity;
    using System.Security.Claims;
    using System.Threading.Tasks;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.EntityFramework;

    public partial class MyUsers : IdentityUser
    {
        public string UserId { get; set; }
        public string LanguageId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Password { get; set; }
        public bool Enabled { get; set; }

        [NotMapped]
        public string PasswordHash { get; set; }

        [NotMapped]
        public string Id
        {
            get { return UserId; }
        }
        [NotMapped]
        public string UserName
        {
            get { return FirstName + " " + LastName; }
            set { }
        }  

        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<MyUsers> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
Note I added 3 NotMapped properties so that MyUsers can implements IdentyUser, which implements IUser Interface. I also added GenerateUserIdentityAsync which is required by IdentityConfig class under App_Start. All left codes are generated from Entity Framework.

Then create a class named UserStoreService which implements IUserStore<MyUsers>, IUserPasswordStore<MyUsers> and your own IService<MyUsers>.
    using System;
    using LinqKit;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Web.UI.WebControls;
    using MyApp.Models;
    using Microsoft.AspNet.Identity;
    public class UserStoreService : Service<MyUsers>, IService<MyUsers>, IUserStore<MyUsers>, IUserPasswordStore<MyUsers>
    {
        public UserStoreService(DbContext db)
            : base(db)
        {
        }

        public UserStoreService()
            : base()
        {
        }

        #region IUserStore implementation
        public Task<MyUsers> FindByIdAsync(string userId)
        {
            Logger.Log("UserStoreService:FindByIdAsync (userId = {0})", userId);

            if (string.IsNullOrEmpty(userId)) return Task.FromResult<MyUsers>(null);
            /*XmlNode n = m_doc.SelectSingleNode(string.Format("/users/user[@id='{0}']", userId));
            MyUsers u = null;
            if (n != null)
            {
                u = new MyUsers { Id = userId, UserName = userId, PasswordHash = n.Attributes["password"].Value };
            }*/
            Task<MyUsers> task = ((MyDBEntities)_db).MyUsers.Where(a => a.Id == userId).FirstOrDefaultAsync();
            return task;
        }

        public Task<MyUsers> FindByNameAsync(string userName)
        {
            Task<MyUsers> task = ((MyDBEntities)_db).MyUsers.Where(a => a.UserName == userName).FirstOrDefaultAsync();
            return task;
        }

        public Task CreateAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task DeleteAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task UpdateAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IUserPasswordStore implementation
        public Task<string> GetPasswordHashAsync(MyUsers user)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }
            return Task.FromResult(user.Password);
        }

        public Task<bool> HasPasswordAsync(MyUsers user)
        {
            return Task.FromResult(user.Password != null);
        }

        public Task SetPasswordHashAsync(MyUsers user, string passwordHash)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IUserLockoutStore implementation

        public Task<int> GetAccessFailedCountAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task<bool> GetLockoutEnabledAsync(MyUsers user)
        {
            //Logger.Log("XmlUserStore:GetLockoutEnabledAsync (user = {0})", user);
            return Task.FromResult<bool>(false);
        }

        public Task<DateTimeOffset> GetLockoutEndDateAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task<int> IncrementAccessFailedCountAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task ResetAccessFailedCountAsync(MyUsers user)
        {
            throw new NotImplementedException();
        }

        public Task SetLockoutEnabledAsync(MyUsers user, bool enabled)
        {
            throw new NotImplementedException();
        }

        public Task SetLockoutEndDateAsync(MyUsers user, DateTimeOffset lockoutEnd)
        {
            throw new NotImplementedException();
        }

        public Task<MyUsers> FindByIdAsync(object userId)
        {
            throw new NotImplementedException();
        }

        #endregion

        //following 3 functions are used to Handle DataTable control for store procedure based object
        public override List<MyUsers> GetDataTableResultByPage(DataTableParameters param, List<MyUsers> list)
        {
            return GetSearchResult(param, list).Skip(param.Start).Take(param.Length).SortBy(param.SortOrder).ToList();
        }

        public override int GetSearchResultCount(DataTableParameters param, List<MyUsers> list)
        {
            return GetSearchResult(param, list).ToList().Count;
        }

        //Search based on param.Search only. 
        //TODO: can add each search for individual columns
        public override IQueryable<MyUsers> GetSearchResult(DataTableParameters param, List<MyUsers> list)
        {
            string search = param.Search.Value;
            return list.AsQueryable().Where(p => (search == null
                || p.UserName != null && p.UserName.ToLower().Contains(search.ToLower())));
        }
    }
Then replacing ApplicationUser with MyUsers from following class:  IdentityConfig class under App_Start, AccountController, ManageController class.  This solution has problem that once you update your model from database in Entity Framework, the MyUsers class will be changed with default codes. It seemed there is no good solution for this. You can extends ApplicationUser from MyUsers, but still MyUsers need to extends IUser.

Final word, if you check the UserStoreService, there are lots of unused code inside. Its a very complex class. I believe Microsoft will change this Identity Framework in future as they did several times before. So if we want to use our own user tables, it's better to implement our own authentication framework.

Reference:
http://aspnetguru.com/customize-authentication-to-your-own-set-of-tables-in-asp-net-mvc-5/
http://www.codeproject.com/Tips/855664/ASP-NET-MVC-Authentication-Using-Custom-UserStore
http://stackoverflow.com/questions/31584506/how-to-implement-custom-authentication-in-asp-net-mvc-5
https://github.com/KriaSoft/AspNet.Identity/blob/master/docs/Database-First.md
http://identity.codeplex.com/
http://odetocode.com/blogs/scott/archive/2013/11/25/asp-net-core-identity.aspx
http://odetocode.com/blogs/scott/archive/2014/01/09/customization-options-with-asp-net-identity.aspx
http://www.khalidabuhakmeh.com/asp-net-mvc-5-authentication-breakdown
http://www.khalidabuhakmeh.com/asp-net-mvc-5-authentication-breakdown-part-deux
http://brockallen.com/2013/10/20/the-good-the-bad-and-the-ugly-of-asp-net-identity/
http://weblogs.asp.net/jeff/decoupling-owin-external-authentication-from-asp-net-identity
http://www.codeproject.com/Articles/875547/Custom-Roles-Based-Access-Control-RBAC-in-ASP-NET
https://lostechies.com/derickbailey/2011/05/24/dont-do-role-based-authorization-checks-do-activity-based-checks/

No comments:

Post a Comment