ASP.NET Core Identity with a custom data store

If you create a new website with ASP.NET Core, and select the option for individual user accounts, you’ll end up with a site that uses the Entity Framework implementation of ASP.NET Core Identity.

I’ve found recently that many people believe that this is all there is to identity 🤷‍♂️

The truth is, “identity”, by which I mean the abstractions provided by Microsoft.AspNetCore.Identity, are incredibly flexible and highly pluggable. It’s far easier to plug your existing users database into these abstractions than it ever was to implement a custom Membership provider 🥳

The first thing to remember is that identity is just a set of common abstractions; by default, the rest of the ASP.NET Core framework (MVC etc) understands these abstractions and will use them for authentication and authorisation.

This means that all you need to do is fill in these interfaces to enable out-of-the-box identity management. Let’s work through an example.

dotnet new classlib -o MyCustomIdentityProvider
dotnet add package Microsoft.AspNetCore.Identity
dotnet add package System.Data.SqlClient
dotnet add package Dapper

Our example is going to use a custom SQL Server database, but you could use anything from Postgres to flat XML files to a WCF service contract to get your data.

In this example we’re going to do a minimal implementation, so we’re going to implement the following interfaces:

There are quite a few interfaces in this namespace, but don’t panic – you only need to implement as many as you need to use (for example, if you’re not using 2-factor authentication, there’s no need to implement IUserTwoFactorStore<TUser>).

The other thing to note is that you need to define your user and role objects first. These don’t have to inherit from anything, so long as they’re classes; you’re free to put whatever you want in here. Let’s sketch out a basic example:

public class MyCustomUser
{
public Guid UserId { get; set; }
public string Username { get; set; }
}

public class MyCustomRole
{
public Guid RoleId { get; set; }
public string RoleName { get; set; }
}

Note that you could go a step further and make these into immutable objects with a bit more effort – I’ve left all properties as get/set for now because it makes this example simpler, but you can go nuts with creating your classes however you want – the power is in your hands!

So far, so simple. Next, we’re going to create a base class for our implementations that can encapsulate data access, and an options object for configuring it:

public class CustomIdentityOptions
{
public string ConnectionString { get; set; }
}

public abstract class CustomIdentityBase
{
private readonly string _connectionString;

protected CustomIdentityBase(IOptions options)
{
_connectionString = options.Value.ConnectionString;
}

protected SqlConnection CreateConnection()
{
var con = new SqlConnection(_connectionString);
con.Open();
return con;
}
}

That’s quite a bit of boilerplate, but it’ll make our lives easier. Next, let’s implement our first interface – IUserStore. To do this, we’ll create a class that extends our base class and implements IUserStore – then, all we need to do is fill in the blanks!

public class CustomUserStore : CustomIdentityBase, IUserStore
{
public CustomUserStore(IOptions options)
: base(options)
{
}

public void Dispose()
{
}

/* WHOAH! That's a LOT of methods to fill in! */
}

Don’t despair! Yes, there are now a lot of methods to fill in. This is actually a good thing. By giving you all of these methods, you now have the power to implement identity with as much flexibility as you’re likely to need. Let’s work through some examples:

public async Task CreateAsync(MyCustomUser user, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
await con.ExecuteAsync("INSERT INTO Users ( UserId, Username ) VALUES ( @UserId, @Username )", user);
return IdentityResult.Success;
}
}

Because the interface is pretty flexible, you can store the user data however you like – in our example, we use ADO.NET and some Dapper magic. Let’s look at some more methods:

public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
return await con.QueryFirstAsync(
"SELECT UserId, Username " +
"FROM Users " +
"WHERE Username = @normalizedUserName",
new { normalizedUserName });
}
}

Easy, right? You can see that it would be very straightforward to plug in a web service call or a Mongo query in here, depending on your infrastructure.

Next up, some methods that might look a bit odd, because in our example we implement them by loading properties from the user object itself. Take a look:

public Task GetNormalizedUserNameAsync(MyCustomUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.Username.ToLowerInvariant());
}

public Task GetUserIdAsync(MyCustomUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.UserId.ToString());
}

public Task GetUserNameAsync(MyCustomUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.Username);
}

That looks odd, but remember that we might be using a TUser object which doesn’t contain this information, in which case we’d need to look it up from a datastore. Additionally, it helps the framework know which properties of our TUser object represent the user ID and user name without having to restrict us to a specified base class.

Next, we have some other slightly odd methods:

public Task SetNormalizedUserNameAsync(MyCustomUser user, string normalizedName, CancellationToken cancellationToken)
{
user.Username = normalizedName;
return Task.CompletedTask;
}

public Task SetUserNameAsync(MyCustomUser user, string userName, CancellationToken cancellationToken)
{
user.Username = userName;
return Task.CompletedTask;
}

Again, these look odd because of the structure of our object, but they would also enable you to call remote services for validation, etc.

If we implemented IUserEmailStore<TUser>, then we could start adding logic for storing email addresses and, importantly, storing whether those addresses are confirmed (useful for scenarios where users must confirm their email before being allowed to log in).


Phew, we’re getting there! Now let’s move on and add support for password validation. We’re going to make a tweak to our user store class:

public class CustomUserStore :
CustomIdentityBase,
IUserStore,
<strong>IUserPasswordStore</strong>

This will now require us to fill in three new methods. In our example, let’s assume that passwords are stored in a separate table (for security reasons):

public async Task SetPasswordHashAsync(MyCustomUser user, string passwordHash, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
// TODO: insert or update logic skipped for brevity
await con.ExecuteAsync(
"UPDATE Passwords SET PasswordPash = @passwordHash " +
"WHERE UserId = @userId",
new { user.UserId, passwordHash });
}
}

public async Task GetPasswordHashAsync(MyCustomUser user, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
return await con.QuerySingleOrDefaultAsync(
"SELECT PasswordHash " +
"FROM Passwords" +
"WHERE UserId = @userId",
new { user.UserId });
}
}

public async Task HasPasswordAsync(MyCustomUser user, CancellationToken cancellationToken)
{
return !string.IsNullOrEmpty(await GetPasswordHashAsync(user, cancellationToken));
}

Again, you can get the password from wherever you want. Perhaps you want to store passwords in a super-secure database with TDE and massively locked-down access; if so then no problem! Just open a different connection. Passwords in a dedicated key store? Fine, we can do whatever we like in this class.

Speaking of passwords and hashing, our datastore uses BCrypt to hash passwords. Let’s implement a basic hasher so that our users can verify their passwords:

dotnet add package BCrypt.Net-Next
public class CustomPasswordHasher : IPasswordHasher
{
public string HashPassword(MyCustomUser user, string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}

public PasswordVerificationResult VerifyHashedPassword(MyCustomUser user, string hashedPassword, string providedPassword)
{
return BCrypt.Net.BCrypt.Verify(providedPassword, hashedPassword) ?
PasswordVerificationResult.Success :
PasswordVerificationResult.Failed;
}
}

For more on using BCrypt and a far more complete implementation of a custom password hasher, see “Migrating passwords in ASP.NET Core Identity with a custom PasswordHasher” by Andrew Lock.


Now we’re going to quickly move on to implement roles – I won’t include all the details because by now this should be pretty familiar. The implementation looks a bit like this:

public class CustomRoleStore : CustomIdentityBase, IRoleStore
{
public CustomRoleStore(IOptions options)
: base(options)
{
}

/* snip */

public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
return await con.QueryFirstOrDefaultAsync(
"SELECT RoleId, RoleName FROM Roles " +
"WHERE RoleName = @normalizedRoleName",
new { normalizedRoleName });
}
}

/* snip */
}

Finally, we need to be able to link our users to roles. Go back to our custom user store and add the following line:

public class CustomUserStore :
CustomIdentityBase,
IUserStore,
IUserPasswordStore,
<strong>IUserRoleStore
</strong>

Now we need to implement a bunch of methods, as so:

public async Task AddToRoleAsync(MyCustomUser user, string roleName, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
await con.ExecuteAsync(
"INSERT INTO UserRoles ( RoleId, UserId ) " +
"SELECT @userId, RoleId " +
"FROM Roles " +
"WHERE RoleName = @roleName",
new { user.UserId, roleName });
}
}

public async Task<IList> GetRolesAsync(MyCustomUser user, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
return (await con.QueryAsync(
"SELECT r.RoleName " +
"FROM UserRoles AS ur " +
"JOIN Roles AS r ON ur.RoleId = r.RoleId " +
"WHERE ur.UserId = @userId",
new { user.UserId })
).ToList();
}
}

public async Task<IList> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
{
using (var con = CreateConnection())
{
return (await con.QueryAsync(
"SELECT u.UserId, u.Username " +
"FROM UserRoles AS ur " +
"JOIN Roles AS r ON ur.RoleId = r.RoleId " +
"JOIN Users AS u ON ur.UserId = u.UserId " +
"WHERE r.RoleName = @roleName",
new { roleName })
).ToList();
}
}

public async Task IsInRoleAsync(MyCustomUser user, string roleName, CancellationToken cancellationToken)
{
var roles = await GetRolesAsync(user, cancellationToken);
return roles.Contains(roleName);
}

And that’s it! We now have a custom bare-bones identity provider. To use this, we can just hook it up in Startup.cs, as follows:

services.Configure(options =>
{
options.ConnectionString = Config.GetConnectionString("MyCustomIdentity");
});

services.AddScoped<IPasswordHasher, CustomPasswordHasher>();
services.AddIdentity()
.AddUserStore()
.AddRoleStore();

 

ASP.NET Core automatic type registration

A little bit of syntactic sugar for you this Friday!

Let’s say we have an application that uses a command pattern to keep the controllers slim. Maybe we have a base command class that looks a bit like:

public abstract class CommandBase<TModel> where TModel : class
{
  protected CommandBase(MyDbContext db)
  {
    Db = db;
  }

  protected MyDbContext Db { get; }

  Task<CommandResult> ExecuteAsync(TModel model);
}

Using a pattern like this means that we can have very slim controller actions where the logic is moved into business objects:

public async Task<IActionResult> Post(
  [FromServices] MyCommand command,
  [FromBody] MyCommandModel model)
{
  if (!ModelState.IsValid)
    return BadRequest(ModelState);
  var result = await command.ExecuteAsync(model);
  return HandleResultSomehow(result);
}

We could slim this down further using a validation filter, but this is good enough for now. Note that we’re injecting our command model in the action parameters, which makes our actions very easy to test if we want to.

The problem here is that, unless we register all of our command classes with DI, this won’t work, and you’ll see an `Unable to resolve service for type` error. Registering the types is easy, but it’s also easy to forget to do, and leads to a bloated startup class. Instead, we can ensure that any commands which are named appropriately are automatically added to our DI pipeline by writing an extension method:

public static void AddAllCommands(this IServiceCollection services)
{
  const string NamespacePrefix = "Example.App.Commands";
  const string NameSuffix = "Command";

  var commandTypes = typeof(Startup)
    .Assembly
    .GetTypes()
    .Where(t =>
      t.IsClass &&
      t.Namespace?.StartsWith(NamespacePrefix, StringComparison.OrdinalIgnoreCase) == true &&
      t.Name?.EndsWith(NameSuffix, StringComparison.OrdinalIgnoreCase) == true);

  foreach (var type in commandTypes)
  {
    services.AddTransient(type);
  }
}

Using this, we can use naming conventions to ensure that all of our command classes are automatically registered and made available to our controllers.

Using the TryGet pattern in C# to clean up your code

Modern C# allows you to declare output variables inline; this has a subtle benefit of making TryFoo methods more attractive and cleaning up your code. Consider this:

public class FooCollection : ICollection<Foo>
{
  // ICollection<Foo> members omitted for brevity
  public Foo GetFoo(string fooIdentity)
  {
    return this.FirstOrDefault(foo => foo.Identity == fooIdentity);
  }
}

// somewhere else in the code
var foo = foos.GetFoo("dave");
if (foo != null)
{
  foo.Fight();
}

Our GetFoo method will return a default of Foo if one isn’t found that matches fooIdentity — code that uses this API needs to know that null indicates that no matching item was found. This isn’t unreasonably, but it does mean that we’re using two lines to find and assign our matching object. Instead, let’s try this approach:

public class FooCollection : ICollection<Foo>
{
  public bool TryGetFoo(string fooIdentity, out Foo fighter)
  {
    figher = this.FirstOrDefault(foo => foo.Identity == fooIdentity);
    return fighter != null;
  }
}

We’ve encoded the knowledge that null means “not found” directly into our method, and there’s no longer any ambiguity about whether we found our fighter or not. Our calling code can now be reduced by a line:

if (foos.TryGetFoo("dave", out foo))
{
  foo.Fight();
}

It’s not a huge saving on its own, but if you have a class of several hundred lines that’s making heavy use of the GetFoo method, this can save you a significant number of lines, and that’s always a good thing.