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.
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
.
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
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;
}
}
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();
}
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;
});
}
}
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();
}
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.
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.
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)
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 returnTask
objects because you're implementing theIPasswordStore
asynchronous interface. You are usingTask.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!