ASP.NET vNext Identity Part 1 – The Database

I’m progressing in my investigation of web applications using the latest tools and now it’s time to tackle identity. I have a few requirements here:

  1. I want an administrative user that doesn’t rely on anything else
  2. I also want to register users via Facebook, Twitter, Google or Microsoft

I’m going to have to get down and dirty with Entity Framework (an ORM for SQL databases) and the ASP.NET Identity 3.0 framework. This post is all about the pre-requisites – getting a database set up that is usable by the identity framework.

I’ve already got a basic MVC6 setup with a controller (HomeController) and View (Index.cshtml) so I’m not going to include those details here. I’m just concentrating on the extra pieces you need for setting up Identity.

I’m going to use a SQL Express database during development since that is located on the local machine and I can blow it away if I need to. I’m planning on using an Azure-based SQL database in production since that allows me to scale my web site using auto-scale in Azure. Both of those use the standard Microsoft SQL Server plugins to connect. I need some packages from NuGet (registered in my project.json file) to effect that. Here is the list of new (additive) packages:

  1. EntityFramework.SqlServer
  2. Microsoft.AspNet.Identity
  3. Microsoft.AspNet.Identity.EntityFramework
  4. Microsoft.Framework.ConfigurationModel.Json

The latter is not strictly needed. It’s used for specifying configuration options in a JSON file and I’m going to want to set certain configuration options up, so a JSON file is a good place for them. There are other ways of handling this.

Talking of the configuration file, I’m going to call my configuration file config.json. I know – it’s an original name. Place the config.json file in the root of your project. Here are the contents:

{
    "AdminUser": {
        "Username": "admin",
        "Password": "changeme"
    },
    "Database": {
        "ConnectionString": "Server=(localdb)\mssqllocaldb;Database=TestDb;Trusted_Connection=True;MultipleActiveResultSets=true"
    }
}

Spoiler: There is a mistake in this file. I’ve deliberately made the mistake so that I can demonstrate something later on and the fix is easy.

The AdminUser block is for specifying the username and password for the administrative user when it is first created. Once it is created, these entries are ignored. The connection string points to a test database on my local SQL Express instance. I will change this when I move to production to point to the Azure database instance. If you already have a database because of your application then you can use that.

Let’s take a look at my Startup class in Startup.cs in pieces (you can string them all together to get the complete picture):

    public class Startup
    {
        public Startup()
        {
            Configuration = new Configuration()
                .AddJsonFile("config.json")
                .AddEnvironmentVariables();
        }

        public IConfiguration Configuration
        {
            get;
            private set;
        }

I first of all load my config.json file into a Configuration object. Then I allow the operator to override any of my settings with environment variables. Louis Dejardin did a really good post on configuration so I won’t go into it here.

        public void ConfigureServices(IServiceCollection services)
        {
            // Entity Framework Service backed by SQL Server
            services.AddEntityFramework()
                .AddSqlServer()
                .AddDbContext(options =>
                    options.UseSqlServer(Configuration.Get("Database:ConnectionString")));

            // Configure the identity service based on our configuration
            services.Configure(options =>
            {
                options.DefaultAdminUserName = Configuration.Get("AdminUser:Username");
                options.DefaultAdminPassword = Configuration.Get("AdminUser:Password");
            });

            // Now add the entity framework backed identity service
            services.AddIdentity()
                .AddEntityFrameworkStores()
                .AddDefaultTokenProviders();

            // ASP.NET MVC6 Service
            services.AddMvc();
        }

Next I am getting into services. Prior to this change I only had the MVC6 service, which is still at the bottom of the method. Let’s take a look at the rest of it. Firstly I add Entity Framework as a service, making it talk to SQL Server and then setting up the connection string from our configuration.

I am then adding a configuration object based on a model (more on that later) – specifically setting up the Admin username and password in that model.

Finally, I’m setting up Identity to use my ApplicationUser model and my ApplicationDbContext object that I’ve used to configure EntityFramework. This is basically the glue between the identity system and Entity Framework (and hence my database).

        public void Configure(IApplicationBuilder app)
        {
            // A 404 Error Page with useful information
            app.UseErrorPage(ErrorPageOptions.ShowAll);

            // Serve up the stuff in wwwroot
            app.UseStaticFiles();

            // Configure ASP.NET MVC6
            app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}"
                    );
                });

            // Set up the Identity database
            IdentityDbOperations.InitializeIdentityDbAsync(app.ApplicationServices).Wait();
        }
    }

The other part of my startup is the Configure method. Here I am just calling an async routine to initialize my database if it isn’t already set up.

There are three models that I need to write:

  1. ApplicationDbContext
  2. ApplicationUser
  3. IdentityDbContextOptions

The ApplicationUser represents the user that is being logged in. It’s modeled after the IdentityUser, so it is fairly straight forward. Similarly, the ApplicationDbContext is modeled after an IdentityDbContext but based on my new ApplicationUser. I put these definitions in the same file (ModelsApplicationDbContext.cs)

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;

namespace AspNetIdentity.Models
{
    public class ApplicationDbContext : IdentityDbContext
    {
    }

    public class ApplicationUser : IdentityUser
    {
    }
}

Defining the ApplicationUser and ApplicationDbContext like this allows me to add things to the model for the user if I so desire. I can’t change the IdentityUser. Having said that, the IdentityUser has a bunch of general properties on a user that are useful already so I’m not sure I’ll be doing much extending. You can view these objects as follows: ApplicationUser is the model for my user. ApplicationDbContext is a database object that knows how to update the database when users get added and removed from my application.

The IdentityDbContextOptions model is used for passing the default Administrator username and password around the application. It’s pretty basic – just two properties:

namespace AspNetIdentity.Models
{
    public class IdentityDbContextOptions
    {
        public string DefaultAdminUserName
        {
            get;
            set;
        }

        public string DefaultAdminPassword
        {
            get;
            set;
        }
    }
}

The final step of the process is to look at the IdentityDbOperations class. This is the class that sets up the initial data. I want to do three things in this class:

  1. Create the database (if it doesn’t exist).
  2. Create an “admin” role (if it doesn’t exist).
  3. Add the admin user to the “admin” role (it it isn’t already there).

In short, if things break – I want to get the database back to a minimally viable state so I can log in again. Here is the code in sections so I can comment.

namespace AspNetIdentity.Models
{
    public class IdentityDbOperations
    {
        // The default administrator role
        const string adminRole = "admin";

        public static async Task InitializeIdentityDbAsync(IServiceProvider serviceProvider)
        {
            using (var db = serviceProvider.GetRequiredService())
            {
                var sqlDb = db.Database as SqlServerDatabase;
                if (sqlDb != null)
                {
                    // Create the database if it does not already exist
                    Debug.WriteLine("InitializeIdentityDbAsync: Ensuring Database exists");
                    await sqlDb.EnsureCreatedAsync();
                    // Create the first user if it does not already exist
                    await CreateAdminUser(serviceProvider);
                }
            }
        }

This is the entry point into the class and is called from my Startup.Configure() method. It ensures the database is created and creates it if the database does not exist (point 1 above). Then it will try to create the admin user (below):

        private static async Task CreateAdminUser(IServiceProvider serviceProvider)
        {
            var options = serviceProvider.GetRequiredService<IOptions>().Options;
            var userManager = serviceProvider.GetRequiredService<UserManager>();
            var roleManager = serviceProvider.GetRequiredService<RoleManager>();

            // If the admin role does not exist, create it.
            Debug.WriteLine("CreateAdminUser: Ensuring Role admin exists");
            if (!await roleManager.RoleExistsAsync(adminRole))
            {
                Debug.WriteLine("CreateAdminUser: Role admin does not exist - creating");
                var roleCreationResult = await roleManager.CreateAsync(new IdentityRole(adminRole));
                DumpIdentityResult("CreateAdminUser: Role Creation", roleCreationResult);
            }
            else
            {
                Debug.WriteLine("CreateAdminUser: Role admin exists");
            }

            // if the user does not exist, create it.
            Debug.WriteLine(String.Format("CreateAdminUser: Ensuring User {0} exists", options.DefaultAdminUserName));
            var user = await userManager.FindByNameAsync(options.DefaultAdminUserName);
            if (user == null)
            {
                Debug.WriteLine("CreateAdminUser: User does not exist - creating");
                user = new ApplicationUser { UserName = options.DefaultAdminUserName };
                var userCreationResult = await userManager.CreateAsync(user, options.DefaultAdminPassword);
                DumpIdentityResult("CreateAdminUser: User Creation", userCreationResult);
                if (userCreationResult.Succeeded)
                {
                    Debug.WriteLine("CreateAdminUser: Adding new user to role admin");
                    var roleAdditionResult = await userManager.AddToRoleAsync(user, adminRole);
                    DumpIdentityResult("CreateAdminUser: Role Addition", roleAdditionResult);
                }
            }
            else
            {
                Debug.WriteLine("CreateAdminUser: User already exists");
            }
        }

This is a little more complex. In identity land there is a user manager – an object that manages, well, users. There is also a role manager – an object that manages roles. Roles are collections of users that share common permissions and are used for authorization within the application. A user does not need to have a role.

The first thing we do is check to see if the role “admin” exists. If it doesn’t, we try to create it. Then we move onto the user. Just like with the role, we check to see if our admin user exists. if the user does not exist, then we create it and add it to the role we just created.

This is one area of the application that doesn’t get exercised much so I’ve added some debugging statements to it. In particular, I want to see the results of all these additions and changes. Each of the changes returns an IdentityResult. In that object are two properties that are interesting:

  1. Succeeded is true if the operation succeeded
  2. Errors is a list of errors that happened if the operation was not successful

I call DumpIdentityResult (below) to dump these out to my Debug window. You can find this window in the Output window (normally at the bottom of the screen) – just select “Show output from: Debug”.

        private static void DumpIdentityResult(string prefix, IdentityResult result)
        {
            Debug.WriteLine(String.Format("{0}: Result = {1}", prefix, result.Succeeded ? "Success" : "Failed"));
            if (!result.Succeeded)
            {
                foreach (var error in result.Errors)
                {
                    Debug.WriteLine(String.Format("--> {0}: {1}", error.Code, error.Description));
                }
            }
        }
    }
}

That’s pretty much it for code. I have not added Identity yet – that’s my next task. However, I want to ensure my database is set up properly. To do this, run the project and wait for your initial Home page to load. Now, let’s go look at the database. With the project still running, use View -> Server Explorer to open up the Server Explorer. It will look like this:

blog-code-0402-1

Right-click on Data Connections and select Add Connection… Enter (localdb)mssqllocaldb in the server name box and click on Refresh. It will look like this:

blog-code-0402-2

Note the drop down for the database name. Select the TestDb database and click on Ok. A new node will appear under the Data Connections – that is your database. Expand it, and then expand the Tables node. It will look something like this:

blog-code-0402-3

You can right-click on any table to see the contents. Just select Show Table Data. We are concerned with three tables. The first is the AspNetRoles table – we created an admin role and it should be there. Luckily for me, it is. The next table is the AspNetUsers table – that holds the user names. Opening that shows a single row with a whole lot of NULL entries – not what I wanted to see. I wanted to see my admin user. The final table is the AspNetUserRoles table – this would contain an association between my role and my user. However my user doesn’t exist and so this table is empty as well. What went wrong?

Fortunately, I added some debugging to my database creation methods. Scrolling back in my Debug window, I see the following:

blog-code-0402-4

I can clearly see from this that the user creation failed (which I knew anyway), but it gives me the reasons. There is a default password complexity requirement and my password does not meet it. I can go ahead and change my password or I can adjust the password complexity requirements. The simplest mechanism right now is to adjust the default password in config.json to match the requirements. I changed mine to Chang3Me!.

See, I told you there was an error!

Once the change is made, I restarted the server and did the same checks as before. Looking at the database now, you can see two rows in the AspNetUsers table – the NULL one and one for the admin user. You will note that the password is hashed and several other fields have been filled in for you.

Also, if you look in the AspNetUserRoles table, you will see a two column row with UserId set to the Id of the user in AspNetUsers and RoleId is set to the Id of the user in AspNetRoles.

Want one of those fancy database diagrams for this database? I did. Unfortunately, the tool that would generate those (SQL Server Data Tools) isn’t out for Visual Studio 2015 yet. So that will have to wait.

In the next post I will take a look at authentication and authorization and see about getting this user to log in to the application.

If you are interested in the code after this project is done, then take a look at my GitHub repository. It’s all in the AspNetIdentity project.

2 thoughts

  1. Hi!
    I’ve created a project from the startup template where you get the identity system prebuilt and the ApplicationDbContext are already there etc. Now I’m needing to add a new dbContext though. But I haven’t managed to do that. Do you know which way to go if I want to add a new dbContext to my project and still keep the ApplicationDbContext?

    Like

    • Hi Jimmy, I haven’t got that far myself yet. You get to see my learning as well. My thinking is that you would just duplicate the code using another DbContext class. Hit up Julie Lerman on Twitter, though. She would know – she wrote the Pluralsight course on Entity Framework.

      Like

Comments are closed.