Identity configuration in ASP.NET can be quite confusing, especially if you want to customize setup properties.
When you use a code-first approach using Entity Framework, you have full control over your user identity options. However when developers deal with bigger projects, they typically prefer to use a table-first approach in which they create the database, then consume the information in the API, and lastly shape it in a way that it makes sense on the front end.
So, configuring identity might work best with a mixed approach.
Imagine that the client of our fictional software company is a huge car manufacturer. They have a lot of shops all around the world. The first and most important feature of their system should be user-management. It should have different types of users: Admin, Shop Manager, and Seller.
See my working version of the project as a reference if you're ever stuck on any of the code.
After we have defined the main properties that a user in our application should have, it is time to start developing the architecture of our system. Since it can scale fast and the database design is important, we will use table-first approach. The first step is to create our database. For this article, I will use SQL Server 2016 in combination with SQL Management Studio.
Below is a SQL Management Studio picture to help you get started with a blank database.
Now, after we have the blank database, we can set up the ASP.NET web API project, which is going to consume the information. Execute the following steps to do it:
2 Add a new web api project to this solution. Name it WebApi. In this way, we can use the template to scaffold our user accounts system.
Be careful and choose Individual User Accounts
as the Authentication
option. In this way, we will have our app's functionalities scaffolded by the project template.
Upon completion, the following set-up should appear in your Solution Explorer:
We will have several important files for the purpose of this article.
• AppStart\IdentityConfig.cs
• AppStart\Startup.Auth.cs
• Models\IdentityModels.cs
• Controllers\AccountController.cs
Now, after we have our database and our asp project created, we should find a way to link them. In order to achieve this, we will create a DbContext by basing it on a connection string, pointing to our database. Open the Web.config file and see what happens between the <connectionStrings>
tags. For now we have only the default connection, which points to an instance of LocalDb. We can also notice that the default ApplicationDbContext class in Models\IdentityModels.cs is based on this connection. Our idea here is to create a new context and then base our ApplicationUserManager
on it. Replace the default string with the string below:
1<add name="SystemUsers" connectionString="Data Source=.;Initial Catalog=CarBusinessDB;Integrated Security = True" providerName="System.Data.SqlClient" />
Be careful when you determine your Data Source
. If you're using a local instance of SQL Server
, stick with the default data source, or .
. If you're using an Azure
database or other type of service, look at sample connection strings before proceeding further.
The next step is to create our own database context, which we can use for storing our users and their properties. In the Models\IdentityModels.cs
, we are going to delete the ApplicationDbContext and paste the following code on its place.
1public class AppUsersDbContext : IdentityDbContext<ApplicationUser>
2{
3 public AppUsersDbContext()
4 : base("SystemUsers", throwIfV1Schema: false)
5 {
6 }
7
8 public static AppUsersDbContext Create()
9 {
10 return new AppUsersDbContext();
11 }
12}
As you can see, we use the new connection from the Web.config
file for the new context by passing its name as a string.
Once we have the connection between the database and the ASP.NET
project, we should configure the built in ApplicationUserManager, so it uses this context instead of the default one, which we have already deleted.
A quick look at both the UserStore
and the ApplicationUserManager
classes:
1namespace Microsoft.AspNet.Identity.EntityFramework
2{
3 //
4 // Summary:
5 // EntityFramework based user store implementation that supports IUserStore, IUserLoginStore,
6 // IUserClaimStore and IUserRoleStore
7 //
8 // Type parameters:
9 // TUser:
10 public class UserStore<TUser> : UserStore<TUser, IdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUserStore<TUser>, IUserStore<TUser, string>, IDisposable where TUser : IdentityUser
11 {
12 //
13 // Summary:
14 // Default constuctor which uses a new instance of a default EntityyDbContext
15 public UserStore();
16 //
17 // Summary:
18 // Constructor
19 //
20 // Parameters:
21 // context:
22 public UserStore(DbContext context);
23 }
24}
1namespace WebApi
2{
3 // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
4
5 public class ApplicationUserManager : UserManager<ApplicationUser>
6 {
7 public ApplicationUserManager(IUserStore<ApplicationUser> store)
8 : base(store)
9 {
10 }
11
12 public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
13 {
14 ///Calling the non-default constructor of the UserStore class
15 var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
16
17 /// Rest of the class ...
18 }
19 }
20}
The ApplicationUserManager calls the constructor of the UserStore, which accepts a DbContext. Then it uses this connection to store the user's data. So, here it is enough just to pass our custom context as a parameter, when the ApplicationUserManager
calls the UserStore
constructor.
Substitute the manager variable with the following code:
1var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<AppUsersDbContext>()));
The next step is to initialize our context each time our application starts, so that the user manager and the context use the same instance.
Go to the AppStart\Startup.Auth.cs
file, where we can see that currently, only the deleted ApplicationDbContext
is initialized.
1public void ConfigureAuth(IAppBuilder app)
2{
3 // Configure the db context and user manager to use a single instance per request
4 app.CreatePerOwinContext(ApplicationDbContext.Create);
5 app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
6
7 // Enable the application to use a cookie to store information for the signed in user
8 // and to use a cookie to temporarily store information about a user logging in with a third party login provider
9 app.UseCookieAuthentication(new CookieAuthenticationOptions());
10 app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
11
12 /// Rest of the class ...
13}
Change the app.CreatePerOwinContext(ApplicationDbContext.Create);
line with the following code:
1app.CreatePerOwinContext(AppUsersDbContext.Create);
Now the user manager and the context use the same instance.
The next part of setting up our API layer will be to include different roles in our application. Again, we will use the Identity
package with the custom DB Context that we have created. While users are managed by the ApplicationUserManager
, roles are managed by the ApplicationRoleManager
.
This time the class and its references are not scaffolded by the template and we should include them by writing a little bit of code. But before going to this step, let us register a the first user in our system.
Run the project. In the documentation of our API, we already have all endpoints of the scaffolded Account Controller
Open Postman
or whichever tool you use for making requests.
Following the documentation, we should pass the sample JSON
format when we make a request for registering a user.
When making the request, make sure you put the exact LocalDb port. This request should return a
200OK
response and create the appropriate tables in our database by inserting the new data into them.
We are ready to open the database and continue.
We notice that a few tables were created because when we made the request first a migration was executed and then our user data was stored in the dbo.AspNetUsers
table. As I already mentioned, this table has only the default columns of the IdentityUser
class. The other two tables that we are interested in are dbo.AspNetRoles
and dbo.AspNetUserRoles
. As you already deduce, the first table will serve as a place to store all role types in our application. The second one will be the junction table that defines the many-to-many relationship between users and roles. If you do not understand the terms related to SQL
don't worry; the Microsoft.AspNet.Identity
package we use deals with the proper usage of our database. Our job is to configure the ASP.NET
app, so it sends the right data to our DB.
To proceed, we will open our API project again and write some code in order to make use of the already created tables.
Open AppStart\IdentityConfig.cs
and place the following snippet after the end of the ApplicationUserManager
class.
1/// <summary>
2/// The role manager used by the application to store roles and their connections to users
3/// </summary>
4public class ApplicationRoleManager : RoleManager<IdentityRole>
5{
6 public ApplicationRoleManager(IRoleStore<IdentityRole, string> roleStore)
7 : base(roleStore)
8 {
9 }
10
11 public static ApplicationRoleManager Create(IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
12 {
13 ///It is based on the same context as the ApplicationUserManager
14 var appRoleManager = new ApplicationRoleManager(new RoleStore<IdentityRole>(context.Get<AppUsersDbContext>()));
15
16 return appRoleManager;
17 }
18}
This code will create the ApplicationRoleManager
class that we are going to use in our Account Controller
when we write the endpoints, that will asssign roles to different users. But before diving into our controller, let us initialize the ApplicationRoleManager
in the same way we did for our ApplicationUserManager
. So, open AppStart\Startup.Auth.cs
file and put the following code into the ConfigureAuth
method:
1public void ConfigureAuth(IAppBuilder app)
2{
3 // Configure the db context and user manager to use a single instance per request
4 app.CreatePerOwinContext(AppUsersDbContext.Create);
5 ///Initializing User Manager
6 app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
7 ///Initializing Role Manager
8 app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
9 ///Rest of the method... Make sure you do not delete anything, since it is related to the token based authorization that we are going to use.
10}
After we do this, we can open some endpoints which are going to take care of assigning roles to users. Go to Controllers\AccountController.cs
file and put the following snippet in it:
1[AllowAnonymous]
2[Route("users/{id:guid}/roles")]
3[HttpPut]
4public async Task<IHttpActionResult> AssignRolesToUser(string id, string[] rolesToAssign)
5{
6 if (rolesToAssign == null)
7 {
8 return this.BadRequest("No roles specified");
9 }
10
11 ///find the user we want to assign roles to
12 var appUser = await this.UserManager.FindByIdAsync(id);
13
14 if (appUser == null || appUser.IsDeleted)
15 {
16 return NotFound();
17 }
18
19 ///check if the user currently has any roles
20 var currentRoles = await this.UserManager.GetRolesAsync(appUser.Id);
21
22
23 var rolesNotExist = rolesToAssign.Except(this.RoleManager.Roles.Select(x => x.Name)).ToArray();
24
25 if (rolesNotExist.Count() > 0)
26 {
27 ModelState.AddModelError("", string.Format("Roles '{0}' does not exist in the system", string.Join(",", rolesNotExist)));
28 return this.BadRequest(ModelState);
29 }
30
31 ///remove user from current roles, if any
32 IdentityResult removeResult = await this.UserManager.RemoveFromRolesAsync(appUser.Id, currentRoles.ToArray());
33
34
35 if (!removeResult.Succeeded)
36 {
37 ModelState.AddModelError("", "Failed to remove user roles");
38 return BadRequest(ModelState);
39 }
40
41 ///assign user to the new roles
42 IdentityResult addResult = await this.UserManager.AddToRolesAsync(appUser.Id, rolesToAssign);
43
44 if (!addResult.Succeeded)
45 {
46 ModelState.AddModelError("", "Failed to add user roles");
47 return BadRequest(ModelState);
48 }
49
50 return Ok(new { userId = id, rolesAssigned = rolesToAssign });
51}
The route for our new endpoint will be users/{id:guid}/roles
. An ID and an array of roles should be the request parameters.
Finally, make an instance of our ApplicationRoleManager
. To do this, add this code snippet in the AccountController
1public class AccountController : ApiController
2{
3 ///...
4 private ApplicationRoleManager _roleManager;
5 ///...
6 public AccountController(ApplicationUserManager userManager,
7 ISecureDataFormat<AuthenticationTicket> accessTokenFormat, ApplicationRoleManager roleManager)
8 {
9 ///Make an instance of the user manager in the controller to avoid null reference exception
10 UserManager = userManager;
11 AccessTokenFormat = accessTokenFormat;
12 ///Make an instance of the role manager in the constructor to avoid null reference exception
13 RoleManager = roleManager;
14 }
15///...
16 public ApplicationRoleManager RoleManager
17 {
18 get
19 {
20 return _roleManager ?? Request.GetOwinContext().GetUserManager<ApplicationRoleManager>();
21 }
22 private set
23 {
24 _roleManager = value;
25 }
26 }
27/// Rest of the class...
28}
Now, we are ready to test our new endpoint by using the ID property of the user that we just added.
First, add an entry in your dbo.AspNetRoles
, which contains the name of the new role. In our case, we will add three of them: Admin, ShopManager, and Seller
. Open your MSMS, and execute the following query:
1INSERT INTO dbo.AspNetRoles
2(Id, Name)
3VALUES
4(1, 'Admin')
5
6INSERT INTO dbo.AspNetRoles
7(Id, Name)
8VALUES
9(2, 'ShopManager')
10
11INSERT INTO dbo.AspNetRoles
12(Id, Name)
13VALUES
14(3, 'Seller')
This should be enough for us to make the request.
Now we have the connection in our database.
By now, we have developed a fully-functional User Accounts system that is able to maintain user registration and different roles.
Now, let's add some additional fields to our accounts system. To keep things simple, I will create just two additional columns in the AspNetUsers
table. The first one will be IsDeleted
, a BIT
column that will serve as a marker used for soft deletion.
The second column will be CompanySecretCode
, an encrypted column that keeps a code that each user can use to access different documents (there is no particular reason for putting such a column in our table, it is just an example of how to encrypt columns when it comes to accounts system).
In soft deletion, we mark an entity as "deleted" but don't actually remove it from the database until we decide that it is extraneous.
Since the package that we are using implements code-first migrations, it is enough just to add the IsDeleted
column to our ApplicationUser
class and then clearly define its default value and further usage.
First, open AppStart\IdentityConfig.cs
file and add the property:
1public class ApplicationUser : IdentityUser
2{
3 // Property used for soft deleting users
4 public bool IsDeleted { get; set; }
5 public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, string authenticationType)
6 {
7 // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
8 var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
9 // Add custom user claims here
10 return userIdentity;
11 }
12}
After this, we can start using our new column immediately. The first and most obvious thing to do is to assign a value to this property when we create the user. For this purpose, replace the Register
method in the AccountController
with the following code:
1 // POST api/Account/Register
2[AllowAnonymous]
3[Route("Register")]
4public async Task<IHttpActionResult> Register(RegisterBindingModel model)
5{
6 if (!ModelState.IsValid)
7 {
8 return BadRequest(ModelState);
9 }
10
11 var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
12
13 //set the IsDeleted property to false
14 user.IsDeleted = false;
15
16 IdentityResult result = await UserManager.CreateAsync(user, model.Password);
17
18 if (!result.Succeeded)
19 {
20 return GetErrorResult(result);
21 }
22
23 return Ok();
24}
Since we have changed our default ApplicationUser
class and our database still does not know about it, we should see an exception when we try to add a new user. The exception reads: 'Consider using Code First Migrations to update the database.'
We should execute a migration that will create the IsDeleted
column in our database. Before doing this, delete the user we have created because it does not have the IsDeleted
column and this is going to be an obstacle when we try to make it non-nullable
. Next:
Package Manager Console
.Default Project
property to WebApi
.Enable-Migrations -contexttypename AppUsersDbContext
Add-Migration Initial
Update-Database
These commands should execute a migration that creates the needed column in the database. Moreover, you should have a new folder called Migrations
in your WebApi
project. There you can see all migrations that you have executed and you can write new ones, in case you need them.
This is the migration code we used above:
1public partial class Initial : DbMigration
2{
3 public override void Up()
4 {
5 AddColumn("dbo.AspNetUsers", "IsDeleted", c => c.Boolean(nullable: false));
6 }
7
8 public override void Down()
9 {
10 }
11}
Now, we are ready to create our new user (follow the same steps that we have used to create the previous one).
Voila! We have our IsDeleted
column as a part of our Application User
entity.
We can proceed to writing code that is going to deal with deleting user. I prefer to work with Stored Procedures
for everything which is not related to default Identity
properties. For this purpose, I will include a procedure that is going to deal with deletion. Open SQL Management Studio
and execute the following query on our CarBusinessDB
.
1CREATE PROCEDURE DeleteUser
2@UserId nvarchar(128)
3AS
4BEGIN
5 -- SET NOCOUNT ON added to prevent extra result sets from
6 -- interfering with SELECT statements.
7 SET NOCOUNT ON;
8
9 UPDATE dbo.AspNetUsers
10 SET IsDeleted = 1
11 WHERE Id = @UserId
12END
13GO
We will configure our AccountController
in a way that by default only the Admin
role has access to all endpoints. Then we will include some exceptional cases, when other roles can perform operations. We do this by adding the Authorize
attribute on the top of our AccountController
and then pass the Roles
that will be authorized by default.
1[Authorize(Roles = "Admin")]
2[RoutePrefix("api/Account")]
3public class AccountController : ApiController
4{
5///Rest of the class ...
6}
So, we authorized only the Admin
role to access our Account
endpoints. But it makes sense for the Seller
to delete users, as well. For this purpose, we override the default authorization for the DeleteUser
endpoint:
1[OverrideAuthorization]
2[Authorize(Roles = "Admin,Seller")]
3[HttpDelete]
4[Route("user/{id:guid}")]
5public IHttpActionResult DeleteUser(string id)
6{
7 ///Rest of the class...
8}
Now, both Admin
and Seller
roles are authorized to make requests to this endpoint. We can add a user to role Seller
and then get a token:
And make a request to the endpoint by passing the token, we have received:
In this way, we are able to use all features of our application the small identity system that we have developed. Fast, simple, and scalable!
ASP.NET Identity
gives you the opportunity to implement various types of membership systems. Depending on the needs of your application, you can use its default properties or add custom ones. Hopefully this tutorial showed you how to handle both scenarios and helps you as you tackle issues dealing with database architecture and design.
Once again, a working version of the project is available.
If you have any questions, feel free to contact me on [email protected]