Accounting and roles with ASP.NET Identity in MVC

This is a simple tutorial on how to set up accounting and roles authorization in an ASP.NET MVC 5 application using ASP.NET Identity framework.
Sample project can be downloaded here https://github.com/mchudinov/AspMvc5Identity

identity

This tutorial is based on chapters 13 and 14 about ASP.NET identity from an excellent book by Adam Freeman “Pro ASP.NET MVC 5 Platform“.

1. Preparation

Create MVC solution and add ASP.NET Identity packages to it:

2. Update the Web.config file

Two changes are required to the Web.config file. The first is a connection string to use with Identity. The second change is to define an application setting that names the class that initializes OWIN middleware.

  <connectionStrings>
    <add name="IdentityDb" 
      providerName="System.Data.SqlClient" 
      connectionString="Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=IdentityDb;Integrated Security=True; />
  </connectionStrings>

  <appSettings>
    <add key="owin:AppStartup" value="AspMvc5Identity.Startup" />    
  </appSettings>

3. Creating the User Class and Role Class

The user class is derived from IdentityUser, which is defined in the Microsoft.AspNet.Identity.EntityFramework namespace. IdentityUser provides the basic user representation. The role class is derived from IdentityRole class.

public class AppUser : IdentityUser
{
	public DateTimeOffset JoinDate { get; set; }
	public DateTimeOffset LastLoginDate { get; set; }
}
public class AppRole : IdentityRole
{
	public AppRole() : base() { }
	public AppRole(string name) : base(name) { }
}

Add any additional properties you need to these classes. In fact these derived classes are needed only if you want to have additional properties. Otherwise use IdentityUser and IdentityRole directly.

4. Creating the Database Context Class and a DB Init Class

The context class is derived from IdentityDbContext<T>, where T is the user class.

    public class AppIdentityDbContext : IdentityDbContext<AppUser>
    {
        public AppIdentityDbContext() : base("IdentityDb")
        {
           Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());
        }

        public static AppIdentityDbContext Create()
        {
            return new AppIdentityDbContext();
        }
    }

Create() method is in use in OWIN start class. OWIN knows the DB context through this method.

My seed class is called IdentityDbInit. I use CreateDatabaseIfNotExists database initializer. As the name suggests, it will create the database if none exists as per the configuration. Seed method creates Administrators role and an admin administrator’s account.

    public class IdentityDbInit : CreateDatabaseIfNotExists<AppIdentityDbContext>
    {
        protected override void Seed(AppIdentityDbContext context)
        {
            AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));
            AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));
            string roleName = "Administrators";
            string userName = "admin";
            string password = "admin1"; //password must be at least 6 characters by default
            string email = "admin@example.com";
            if (!roleMgr.RoleExists(roleName))
            {
                roleMgr.Create(new AppRole(roleName));
            }
            AppUser user = userMgr.FindByName(userName);
            if (user == null)
            {
                userMgr.Create(new AppUser { UserName = userName, Email = email }, password);
                user = userMgr.FindByName(userName);
            }
            if (!userMgr.IsInRole(user.Id, roleName))
            {
                userMgr.AddToRole(user.Id, roleName);
            }
            base.Seed(context);
        }
    }

Read more here about Database Initialization Strategies in Entity Framework.

5. Creating Manager Classes for Users and Roles

Manager classes are used by controllers to execute CRUD actions on users and role.

User manager class manages instances of the user class. The user manager class must be derived from UserManager<T>, where T is the user class.

public class AppUserManager : UserManager<AppUser>
{
	public AppUserManager(IUserStore<AppUser> store) : base(store) {}

	public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
	{
		AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
		AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));

		manager.UserValidator = new UserValidator<AppUser>(manager)
		{
			AllowOnlyAlphanumericUserNames = true,
			RequireUniqueEmail = true
		};
		return manager;
	}
}

The RoleManager is accordingly derived form RoleManager<T> where T is a role class.

public class AppRoleManager : RoleManager<AppRole>
{
	public AppRoleManager(RoleStore<AppRole> store): base(store) {}

	public static AppRoleManager Create(IdentityFactoryOptions<AppRoleManager> options, IOwinContext context)
	{
		return new AppRoleManager(new RoleStore<AppRole>(context.Get<AppIdentityDbContext>()));
	}
}

 

6. Creating OWIN Startup Class

Start class starts ASP.NET Identity according to OWIN specification. The name of this class is used in Web.config
<add key="owin:AppStartup" value="AspMvc5Identity.Startup" />

public class Startup
{
	public void Configuration(IAppBuilder app)
	{
		app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);
		app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
		app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
		app.UseCookieAuthentication(new CookieAuthenticationOptions
		{
			AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
			LoginPath = new PathString("/Account/Login"),
		});
	}
}

7. Account Controller and Login View

Controller is needed for variuos authentication scenarious. I use simple form authentication in this tutorial. We need a account controller and a login view for it as it is mentioned in IndentityConfig class in LoginPath:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
	AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
	LoginPath = new PathString("/Account/Login"),
});

Account controller needs methods for login and logout:

[Authorize]
public class AccountController : Controller
{
	private IAuthenticationManager AuthManager => HttpContext.GetOwinContext().Authentication;
	private AppUserManager UserManager => HttpContext.GetOwinContext().GetUserManager<AppUserManager>();

	[AllowAnonymous]
	public ActionResult Login()
	{
		if (HttpContext.User.Identity.IsAuthenticated)
		{
			return View("Error", new [] { "Access Denied. Already logged in." });
		}
		return View();
	}        

	[HttpPost]
	[AllowAnonymous]
	[ValidateAntiForgeryToken]
	public async Task<ActionResult> Login(LoginModel details)
	{
		if (ModelState.IsValid)
		{
			AppUser user = await UserManager.FindAsync(details.Name, details.Password);
			if (user == null)
			{
				ModelState.AddModelError("", "Invalid name or password.");
			}
			else
			{
				ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
				AuthManager.SignOut();
				AuthManager.SignIn(new AuthenticationProperties{IsPersistent = false}, ident);
				return RedirectToAction("Index", "UserAdmin");
			}
		}
		return View(details);
	}

	[Authorize]
	public ActionResult Logout()
	{
		AuthManager.SignOut();
		return RedirectToAction("Index", "Home");
	}
}

Login view:

@model AspMvc5Identity.Models.LoginModel
<h2>Log In</h2>
@Html.ValidationSummary()
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken();
    <div>
        <label>Name</label>
        @Html.TextBoxFor(x => x.Name)
    </div>
    <div>
        <label>Password</label>
        @Html.PasswordFor(x => x.Password)
    </div>
    <button type="submit">Log In</button>
}

8. Users and Roles Controllers

Now we need to create controllers to operate over users and roles. Controllers need Index, Create, Delete and Edit methods.

[Authorize(Roles = "Administrators")]
public class UserAdminController : Controller
{
	private AppUserManager UserManager => HttpContext.GetOwinContext().GetUserManager<AppUserManager>();

	public ActionResult Index()
	{
		return View(UserManager.Users);
	}

	public ActionResult Create()
	{
		return View();
	}

	[HttpPost]
	public async Task<ActionResult> Create(CreateModel model)
	{
		if (ModelState.IsValid)
		{
			AppUser user = new AppUser { UserName = model.Name, Email = model.Email };
			IdentityResult result = await UserManager.CreateAsync(user,model.Password);
			if (result.Succeeded)
			{
				return RedirectToAction("Index");
			}
			else
			{
				AddErrorsFromResult(result);
			}
		}
		return View(model);
	}

	[HttpPost]
	public async Task<ActionResult> Delete(string id)
	{
		AppUser user = await UserManager.FindByIdAsync(id);
		if (user != null)
		{
			IdentityResult result = await UserManager.DeleteAsync(user);
			if (result.Succeeded)
			{
				return RedirectToAction("Index");
			}
			else
			{
				return View("Error", result.Errors);
			}
		}
		else
		{
			return View("Error", new [] { "User Not Found" });
		}
	}
	
	private void AddErrorsFromResult(IdentityResult result)
	{
		foreach (string error in result.Errors)
		{
			ModelState.AddModelError("", error);
		}
	}
}

 

9. Views for Users and Roles

An example of Index view for users:

@model IEnumerable<AppUser>
<div>
    <div>
        Users
    </div>
    <table>
        <tr><th>ID</th><th>Name</th><th>Email</th><th></th></tr>
        @if (!Model.Any())
        {
            <tr><td colspan="4">No User Accounts</td></tr>
        }
        else
        {
            foreach (AppUser user in Model)
            {
                <tr>
                    <td>@user.Id</td>
                    <td>@user.UserName</td>
                    <td>@user.Email</td>
                    <td>
                        @using (Html.BeginForm("Delete", "UserAdmin",new { id = user.Id }))
                        {
                            @Html.ActionLink("Edit", "Edit", new { id = user.Id }, null)
                            <button type="submit">Delete</button>
                        }
                    </td>
                </tr>
            }
        }
    </table>
</div>

10. Enable database migrations

ASP.NET Identity uses an Entity Framework behind the scene which uses a database as a backend storage. As soon as user or role data model in the application is changed the database must be changed accordingly or application will not work. Thus we need data migrations with code-first scenario.

Code-first scenario needs three steps in Package Manager Console command line.

10.1 Enable-Migrations command

In case we use ASP.NET Identity side by side with another Entity Framework database context with enabled migrations, which is very likely in a real world application, we might need a couple of advanced parameters here.

Enable-Migrations -Verbose 
-ContextProjectName AspMvc5Identity 
-StartUpProjectName AspMvc5Identity 
-ProjectName AspMvc5Identity  
-ConnectionStringName IdentityDb 
-MigrationsDirectory Migrations 
-ContextTypeName AppIdentityDbContext

I split the command in several lines, but it should be a single line as a real command.

Explanation:
-ContextProjectName
Specifies the project name which contains the DbContext class to use.
-StartUpProjectName
Specifies the project name which contains configuration file to use for named connection strings. This is important if application’s start class is not in the same project where database context is placed.
-ProjectName
Specifies the project that the scaffolded migrations configuration class will be added to.
-ConnectionStringName
Specifies the the connection string to use. This is only needed if ASP.NET Identity uses another database then the rest of application.
-MigrationsDirectory
Specifies the name of the directory that will contain migrations code files. This is important if Identity DB context lives in the same project as applications DB context. In this case migrations folders for Identity and for the rest of application must be different. Which we can specify using this key.
-ContextTypeName
Specifies the Identity database context class to use.

10.2 Add-Migration command

Add-migration Init -Verbose 
-StartUpProjectName AspMvc5Identity 
-ProjectName AspMvc5Identity 
-ConnectionStringName IdentityDb

-StartUpProjectName
Specifies the project name which contains configuration file to use for named connection strings.
-ProjectName
Specifies the project that the scaffolded migrations configuration class will be added to.
-ConnectionStringName
Specifies the the connection string to use. This is only needed if ASP.NET Identity uses another database then the rest of application.

10.3 Update-Database command

Update-Database -Verbose 
-StartUpProjectName AspMvc5Identity 
-ProjectName AspMvc5Identity 
-ConnectionStringName IdentityDb