The finishing touches to hooking into ASP.NET Core Identity user creation

Setting up ASP.NET Core Identity to reject registrations where the registrant doesn't exist in another database

In my previous posts I've setup ASP.NET Core Identity, tweaked the IDs (just because I can), extended the Identity so it stores extra data and started the process of pulling it all together by extending the UserManager so I can hook into the registration process.

Related posts:

  1. Taking the GUID out of ASP.NET Core Identity
  2. Splitting out ASP.NET Core Identity into a separate library
  3. Extending the ASP.NET Core Identity user
  4. Reading and writing custom ASP.NET Core Identity user properties
  5. Extending the ASP.NET Core Identity UserManager to set the Employee Id during registration
  6. The finishing touches to hooking into ASP.NET Core Identity user creation (this post)

Adding a Database Context for the Applications data

In order to fully join the dots a library to contain the data access code for the application database is needed, so I added that using Visual Studio's "New Project" tooling, then added a reference to Entity Framework Core with:

Install-Package Microsoft.EntityFrameworkCore.SqlServer -ProjectName RW.HumanResourcesPortal.Data

Once Visual Studio had finished installing that, along with all the various dependencies, next up was adding models and a database context. For the purposes of this post I'm only going to document one of the models that was added, which is the model for Employee:

public class Employee
{
    public int EmployeeId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public Employee Manager { get; set; }
}

Along with the database context:

public sealed class HumanResourcesDatabaseContext : DbContext
{
    public HumanResourcesDatabaseContext(DbContextOptions<HumanResourcesDatabaseContext> options) :
        base(options)
    {
    }

    public DbSet<Employee> Employees { get; set; }
}

With the database context and model present, next I can add a reference to the new project to the .Web and .Identity projects and update the .Web projects Startup.cs so that it's aware of the new type of Database Context:

services.AddDbContext<HumanResourcesDatabaseContext>(options =>
    options.UseSqlServer(
        Configuration.GetConnectionString("ApplicationConnection")));

Along with an additional entry in the ConnectionStrings section of appsettings.json for the new connection string. With that done, the next thing to do is create the initial database creation / migration code by using the dotnet command from the project directory:

md Migrations
dotnet ef migrations add InitialModel -s ..\RW.HumanResourcesPortal.Web --context HumanResourcesDatabaseContext

The tooling spits out the message:

info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.1.4-rtm-31024 initialized 'HumanResourcesDatabaseContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'

And looking back to Visual Studio there's now three files in the Migrations folder, two for the initial migration (creation!) and one HumanResourcesDatabaseContextModelSnapshot. Before going any further it's a good idea to make sure the application will still run, I'm a big proponent of making small changes and then validating them - iterating and validating rapidly is a good way to ensure you don't go too far down a rabbit hole of broken code or false assumptions. Writing copious unit tests is a great way to do this, though there's not really many good candidates for the boilerplate code generated thus far.

Hitting F5 in Visual Studio shows nothing's broken so far... A little bit of throw-away code in HomeController will do the job of triggering the EFCore migration and making sure the Employee table gets created correctly:

public HumanResourcesDatabaseContext HRDbContext { get; set; }

public HomeController(UserManager<ApplicationIdentityUser> userManager, ApplicationDbContext dbContext, HumanResourcesDatabaseContext hrdbContext)
{
    UserManager = userManager;
    DbContext = dbContext;
    HRDbContext = hrdbContext;
}

public async Task<IActionResult> Index()
{
    var employees = HRDbContext.Employees.ToList();
    ...
    ...
}

Another trip to the debugger by hitting F5 and up pops the usual page in the browser:

After adding code that tries to retrieve Employees, a prompt is given to apply the migrations required because the table cannot be found

After clicking on Apply Migrations the button disabled itself and a few seconds later the text Try refreshing the page appears next to it. After doing just that the homepage loads without any issue - a quick peek at the database shows the table has been created correctly:

Checking that the backing table for the Employees entities has been created properly

Aside: As you can see, Azure Data Studio thinks that the object doesn't exist! In order to fix that you can press CTRL-SHIFT-P to open the Command Palette and search for 'intellisense'. As of the time of writing this only returns one item, Refresh Intellisense Cache. Once that's triggered, it's squiggles begone!

Bringing it all together

Now there's an Employees table to look at, it's time to join the dots and have the registration process check that the user registering is an employee before letting them complete registration. First up is extending the ApplicationIdentityUserManager (the custom UserManager I created in the last post) by adding the new database context to its constructor:

public class ApplicationIdentityUserManager : UserManager<ApplicationIdentityUser>
{
    public HumanResourcesDatabaseContext HrDbContext { get; set; }

    public ApplicationIdentityUserManager(IUserStore<ApplicationIdentityUser> store, IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<ApplicationIdentityUser> passwordHasher, IEnumerable<IUserValidator<ApplicationIdentityUser>> userValidators,
        IEnumerable<IPasswordValidator<ApplicationIdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<ApplicationIdentityUser>> logger,
        HumanResourcesDatabaseContext hrDbContext)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
        HrDbContext = hrDbContext;
    }
}

With that in place the CreateUser override can be tweaked to check for a matching record in the Employees table, which means that attempts to register by non-employees can be rejected. Or it would be if I'd remembered to add EmailAddress as a property of the Employee model when I created it! So, having added the property and then added a migration via the dotnet command:

dotnet ef migrations add AddEmailAddress -s ..\RW.HumanResourcesPortal.Web --context HumanResourcesDatabaseContext

With that mistake corrected, it's just a matter of overriding the CreateAsync method in ApplicationIdentityUserManager with something that checks the Employees table for a matching user:

public override async Task<IdentityResult> CreateAsync(ApplicationIdentityUser user)
{
    var matchingUser = HrDbContext.Employees.FirstOrDefault(candidateUser => candidateUser.EmailAddress.Equals(user.Email, StringComparison.OrdinalIgnoreCase));

    if (matchingUser == null)
    {
        return IdentityResult.Failed(new IdentityError { Description = "Sorry, your email address isn't recognised as an employee" });
    }
user.EmployeeId = matchingUser.EmployeeId; return await base.CreateAsync(user); }

Now trying to register with an email address that doesn't exist in the Employees table kicks out:

Trying to register a user account when the email address doesn't exist in the Employees table gets rejected

After all that, adding a record to the Employees table for [email protected]:

INSERT
INTO    dbo.Employees
        (
            FirstName,
            LastName,
            DateOfBirth,
            EmailAddress
        )
VALUES  (
            'Davey',
            'Jones',
            '1900-01-01',
            '[email protected]'
        )

And attempting to register with that email address gives the result we're looking for:

Davey Jones was allowed to register!

Summing up

With all that done there's now a web application that accepts self-registration, but only if the user registering matches one in a separate database/database context (If I was doing this properly there would almost certainly be a UserService that was passed into the ApplicationIdentityUserManager rather than a database context which would let me separate out the implemetnation detail of an Entity Framework Core backed employee store). It all works pretty well and will lead on quite readily to adding features like 'if the user has a LeavingDate, which has passed, don't let them login' by hooking into other overridable methods of the UserManager class.

About Rob

I've been interested in computing since the day my Dad purchased his first business PC (an Amstrad PC 1640 for anyone interested) which introduced me to MS-DOS batch programming and BASIC.

My skillset has matured somewhat since then, which you'll probably see from the posts here. You can read a bit more about me on the about page of the site, or check out some of the other posts on my areas of interest.

No Comments

Add a Comment