Thursday, May 8, 2014

Hacky In Memory User Store for ASP.NET Identity 2.0

There are cases when you want to build out a sample ASP.NET Web project (MVC in this case) just to test something out.  I personally hate the defaults that come with most .net mvc projects.  It forces you to create a database, initialize that database, and other useless steps.  Furthermore, I hate working from the predefined templates that come with MVC.  I like that you can get a working app quickly, but it really doesn't help you understand exactly what is going on.

In previous versions of MVC, you would just create a custom membership provider, override a few methods, register it in the web.config and you were off.  Identity 2.0 changed all of that.

In this post I will try and outline what steps I took to create an in memory user store with very limited functionality.  Bear in mind the goal of this article is to get you going in the right direction, and is probably not a complete solution.

The first thing you are going to want to do is read a couple of MS articles to get a good base line.

Adding Identity 2.0 to an empty project
Overview of Custom Storage Providers for ASP.NET Identity

There are a couple of key components here.  The first in managers.  There are built in managers, which are easy enough to use.  The problem is that there is no source code released for the Microsoft.AspNet.Identity, so it is hard to see exactly what the built in user manager does.  It obviously has to interface with the authorization/authentication framework in MVC, but little detail on that has been released.

The second component is the "stores".  Instead of having one big store (or two as in MVC4) they have broken up the stores into various interfaces.  Each store adds a certain amount of functionality to the user store.  It is understood that the manager knows what type of "store" it has received and which interfaces are implemented.  Based on these interfaces, different methods are called and different functionality is available.

The first step in this process is to create an user object.  At a minimum, this user object must implement IUser which only requires a string id and string password.


    public class IdentityUser : IUser
    {
        public IdentityUser(string id, string userName, string hashedPassword)
        {
            Id = id;
            UserName = userName;
            this.hashedPassword = hashedPassword;
        }

        public IdentityUser(string userName)
        {
            UserName = userName;
        }

        public string Id { get; private set; }
        public string UserName { get; set; }
        public string hashedPassword { get; set; }
    }

In the implementation above, I am adding hashedPassword to the object.  You can add any fields you like.  This is probably a hack and probably not the right place to add it, but I am just trying to get something up quickly.

After the user object is created, you need to create a store.  Depending on the features you want to implement, you will implement different interfaces.  See the links above for more.


    public class InMemoryUserStore : IUserStore<identityuser>, IUserPasswordStore<identityuser>
    {
        private IList<identityuser> userList = new List<identityuser>();

        public InMemoryUserStore()
        {
            userList.Add(new IdentityUser("1", "admin", Crypto.HashPassword("admin")));
        }

        public void Dispose()
        {
            
        }

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

        public Task UpdateAsync(IdentityUser user)
        {
            throw new NotImplementedException();
        }

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

        public Task<identityuser> FindByIdAsync(string userId)
        {
            return Task.FromResult(userList.FirstOrDefault(x => x.Id.Equals(userId)));
        }

        public Task<identityuser> FindByNameAsync(string userName)
        {
            return Task.FromResult(userList.FirstOrDefault(x => x.UserName.Equals(userName)));
        }

        public Task SetPasswordHashAsync(IdentityUser user, string passwordHash)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetPasswordHashAsync(IdentityUser user)
        {
            return Task.FromResult(userList.First(x => x.UserName.Equals(user.UserName)).hashedPassword);
        }

        public Task<bool> HasPasswordAsync(IdentityUser user)
        {
            var appUser = userList.FirstOrDefault(x => x.UserName.Equals(user.UserName));
            if (appUser == null)
            {
                return Task.FromResult(false);
            }

            return Task.FromResult(!String.IsNullOrEmpty(appUser.hashedPassword));
        }
    }

As you can see from above, I only implemented a few of the methods.  I'm not worried about registration, or edit functionality.  I just need to create an account to login with that plugs in with the built in authentication and authorization frameworks.  As you can see from above, I initialize the user in the constructor. 

After this is done, you can just initialize the user manager and pass it the store that you have created.


    [Authorize]
    public class BaseController: Controller
    {
        public BaseController() : this(new UserManager<identityuser>(new InMemoryUserStore()))
        {
            
        }

        private BaseController(UserManager<identityuser> userManager)
        {
            UserManager = userManager;
        }

        public UserManager<identityuser> UserManager { get; private set; }

        protected IAuthenticationManager AuthenticationManager
        {
            get { return HttpContext.GetOwinContext().Authentication; }
        }
 
    }

After this, you have to actually log the user in using the OWIN providers.  You can see samples of this on the internet or if you create a MVC app template with authentication enabled.

Hope that helps!

No comments:

Post a Comment