DEV Community

Carl Layton
Carl Layton

Posted on • Originally published at carlserver.com

Create Custom User Store for ASP .NET Core Identity

In the previous post I looked at how to create a custom password hasher for ASP .NET Core Identity. In this post, I continue customizing the Identity system by creating a custom User Store. This is done by providing custom implementations to the IUserPasswordStore<applicationuser> and IUserEmailStore<applicationuser> interfaces. I'm going to create in-memory implementations of these two interfaces. This will not work in a real application because all the users are deleted when the application restarts but is good for demo purposes. This tutorial requires Visual Studio 2017 and dotnet core 2.0. The entire sample project is available on Github.

Create Starting Application

The first step is to create a new ASP .NET Core web app with individual user accounts. We're going to start with this template because a lot of the boilerplate code is created automatically and it's a good starting point to customize ASP .NET identity. Open Visual Studio 2017 and create a new ASP .NET Core Web Application project. Provide a name and location and click OK.

create new project

Choose .NET Core and ASP.NET Core 2.0 from the dropdowns at the top. Select Web Application (Model-View-Controller) for the template and select Change Authentication and pick Individual User Accounts.

select properties

After the project is created, debug it from Visual Studio to make sure the template is working. After the web app loads, stop debugging. Since we're creating a custom in-memory user store, we don't need the database so you can delete the Migrations folder out of the project's Data folder

delete migrations folder

At this point we have an ASP .NET core web app project with basic user authentication support. In the sections that follow, we will create our custom implementation.

Create the "Data Access" class

Since, we're storing users in memory, we need a place to keep them. We're going to create a custom class for this. This class has nothing to do with ASP .NET Core Identity but we need a place to perform basic CRUD operations on our list of in-memory users. Create a new C# class and name it InMemoryUserDataAccess.cs. The full class is below to copy and paste.

    public class InMemoryUserDataAccess
    {
        private List<ApplicationUser> _users;
        public InMemoryUserDataAccess()
        {
            _users = new List<ApplicationUser>();
        }
        public bool CreateUser(ApplicationUser user)
        {
            _users.Add(user);
            return true;
        }

        public ApplicationUser GetUserById(string id)
        {
            return _users.FirstOrDefault(u => u.Id == id);
        }

        public ApplicationUser GetByEmail(string email)
        {
            return _users.FirstOrDefault(u => u.NormalizedEmail == email);
        }

        public ApplicationUser GetUserByUsername(string username)
        {
           return _users.FirstOrDefault(u => u.NormalizedUserName == username);
        }

        public string GetNormalizedUsername(ApplicationUser user)
        {
            return user.NormalizedUserName;
        }

        public bool Update(ApplicationUser user)
        {
            // Since get user gets the user from the same in-memory list,
            // the user parameter is the same as the object in the list, so nothing needs to be updated here.
            return true;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Next, we need to add this to the ConfigureServices method in the Startup class for dependency injection. Since it's an in-memory list, we will use a singleton. Add services.AddSingleton<InMemoryUserDataAccess>();. The ConfigureServices method is below.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddSingleton<InMemoryUserDataAccess>();
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // Add application services.
        services.AddTransient<IEmailSender, EmailSender>();
        services.AddMvc();
    }
Enter fullscreen mode Exit fullscreen mode

Create Custom User Store

We're finally ready to create our custom user store. The user store in ASP .NET identity can be a complex system of functionality. Luckily, this functionality is broken out into a series of interfaces so we can choose what functionality we want our user store to support. To keep it simple, we're going to implement the IUserPasswordStore and IUserEmailStore interfaces. This is enough to get us started. There are a lot of other interfaces for handling claims, phone numbers, 2 factor authentication, account lockout, etc. This microsoft doc goes into a lot more detail on all the store interfaces.

The full implementation is below. Notice the dependency to InMemoryUserDataAccess we created above. I did not implement delete. I'll leave that up to you.

    public class InMemoryUserStore : IUserPasswordStore<ApplicationUser>, IUserEmailStore<ApplicationUser>
    {
        private InMemoryUserDataAccess _dataAccess;
        public InMemoryUserStore(InMemoryUserDataAccess da)
        {
            _dataAccess = da;
        }

        public Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<IdentityResult>.Run(() =>
            {
                IdentityResult result = IdentityResult.Failed();
                bool createResult = _dataAccess.CreateUser(user);

                if (createResult)
                {
                    result = IdentityResult.Success;
                }

                return result;
            });
        }

        public Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {

        }

        public Task<ApplicationUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
        {
            return Task<ApplicationUser>.Run(() =>
            {
                return _dataAccess.GetByEmail(normalizedEmail);
            });
        }

        public Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            return Task<ApplicationUser>.Run(() =>
            {
                return _dataAccess.GetUserById(userId);
            });
        }

        public Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            return Task<ApplicationUser>.Run(() =>
            {
                return _dataAccess.GetUserByUsername(normalizedUserName);
            });
        }

        public Task<string> GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
                return Task<string>.Run(() =>
                {
                    return user.Email;
                });
        }

        public Task<bool> GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<bool>.Run(() =>
            {
                return user.EmailConfirmed;
            });
        }

        public Task<string> GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<string>.Run(() =>
            {
                return user.NormalizedEmail;
            });
        }

        public Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<string>.Run(() =>
            {
                return user.NormalizedUserName;
            });
        }

        public Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<string>.Run(() => { return user.PasswordHash; });
        }

        public Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<string>.Run(() =>
            {
                return user.Id;
            });
        }

        public Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<string>.Run(() =>
            {
                return user.UserName;
            });
        }

        public Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<bool>.Run(() => { return true; });
        }

        public Task SetEmailAsync(ApplicationUser user, string email, CancellationToken cancellationToken)
        {
            return Task.Run(() => {
                user.Email = email;
            });
        }

        public Task SetEmailConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                user.EmailConfirmed = confirmed;
            });
        }

        public Task SetNormalizedEmailAsync(ApplicationUser user, string normalizedEmail, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                user.NormalizedEmail = normalizedEmail;
            });
        }

        public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                user.NormalizedUserName = normalizedName;
            });
        }

        public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
        {
            return Task.Run(() => { user.PasswordHash = passwordHash; });
        }

        public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                user.UserName = userName;
                user.NormalizedUserName = userName.ToUpper();
            });
        }

        public Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task<IdentityResult>.Run(() =>
            {
                IdentityResult result = IdentityResult.Failed();
                bool updateResult = _dataAccess.Update(user);

                if (updateResult)
                {
                    result = IdentityResult.Success;
                }

                return result;
            });
        }
    }
Enter fullscreen mode Exit fullscreen mode

The dependency injection support in ASP .NET Core MVC makes it easy to use our implementation. We need to add another line to the ConfigureServices method in the Startup class. Add this right after the singleton for InMemoryUserDataAccess. services.AddTransient<IUserStore<ApplicationUser>, InMemoryUserStore>();. The complete ConfigureServices method is below

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddSingleton<InMemoryUserDataAccess>();
        services.AddTransient<IUserStore<ApplicationUser>, InMemoryUserStore>();
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // Add application services.
        services.AddTransient<IEmailSender, EmailSender>();
        services.AddMvc();
    }
Enter fullscreen mode Exit fullscreen mode

Test It

Build and debug the project from Visual Studio. Click the Register button in the upper right corner. Enter an email and password and confirm password and click Register. The password requirements are still the default for ASP .NET Core Identity so you will need a lowercase letter, uppercase letter, number, and special character.

register user

It should redirect to the home page and the Register button should be replaced with a greeting Hello ! Click the Log out link next to this greeting and it redirects to the home page with the Register and Log in buttons.

Click login and use the email and password just created. After login, you are redirected to the homepage with the same greeting.

login user

If you stop debugging and restart the application, the user no longer exists because it's stored in memory.

Conclusion

That concludes how to setup a basic custom user store for ASP .NET Core Identity. We created a custom class implementing only the interfaces we needed and the built in dependency injection makes it easy to swap implementations. User management is a complex topic with many features available for both security and user experience. Hopefully, this is a good starting point for customizing specific features around user management in your ASP .NET Core application.

Top comments (1)

Collapse
 
un1r8okq profile image
Will • Edited

Thanks for sharing this tutorial, I found it very helpful. There are some performance concerns with your use of the C# asynchronous programming model that I thought were worth mentioning.

As you know, your InMemoryUserStore methods need to return Task objects because you're implementing the IPasswordStore asynchronous interface. You are using Task.Run(() => {}) to achieve this, which queues your work to run on the managed thread pool . This has overhead of waiting for a thread, moving data over to it, running the code, giving the thread back to the pool etc. Because of this, Task.Run is generally reserved for CPU intensive tasks where you need to free up the main thread.

In this example, you should be returning Task.CompletedTask from methods returning a Task, and Task.FromResult for methods returning an IdentityResult. This executes your code synchronously and returns an already completed task to the caller without the overhead of thread scheduling. There's a similar answer to this on Stackoverflow from i3arnon which is also worth a read.

I hope this helps!