Use of Identity framework in ASP.NET Core

Identity framework:

1. Adopt the Role-Based Access Control (RABC) strategy, which has built-in management of user, role and other tables and related interfaces.
2. The Identity framework uses EF Core to operate the database.

Identity framework usage

Since the Identity framework uses EF Core to operate the database, the operation has similar expenditures as defining EF Core.

1. Define entity classes:
The Identity framework has helped us define two entity classes, IdentityUser and IdentityRole, where TKey represents the type of primary key. Although the attributes in the defined entity classes are enough for us to use, if necessary we will still define two entity classes that inherit IdentityUser and IdentityRole to add custom attributes.

IdentityRole entity class: used to define role data. For example, different roles have different operation permissions.
 /// <summary>
    /// Verify whether the user has permission to perform some operations
    /// </summary>
    public class MyRole : IdentityRole<long>
    {<!-- -->
        //IdentityRole entity also defines many columns, which can be added in subclasses
    }
IdentityUser entity class: user-defined user information, used to verify user login, etc.
 //Verify whether the user is logged in
    public class MyUser : IdentityUser<long>
    {<!-- -->
        //Many columns have been defined in the IdentityUser entity
        public string? WeiChart {<!-- --> get; set; }
    }

2. Install the NuGet package used by the Identity framework

Install-Package microsoft.AspNetCore.Identity.EntityFrameworkCore
Identity framework Nuget package

3. Define entity configuration class
Like EF Core, define the configuration class of the entity. Of course, you can also not configure it here and just ask it to use the default configuration.

IdentityRole entity configuration class:
 public class MyRoleConfig : IEntityTypeConfiguration<MyRole>
    {<!-- -->
        public void Configure(EntityTypeBuilder<MyRole> builder)
        {<!-- -->
            builder.ToTable("T_MyRole");//Specify the table name
        }
    }
IdentityUser entity configuration class:
 public class MyUserConfig : IEntityTypeConfiguration<MyUser>
    {<!-- -->
        public void Configure(EntityTypeBuilder<MyUser> builder)
        {<!-- -->
            builder.ToTable("T_MyUser");//Specify the table name
        }
    }

4. Create the DbContext class of the Identity framework
Inherit the IdentityDbContext generic class, where the first generic is IdentityUser or a class that inherits the entity, the second generic is IdentityRole or a class that inherits the entity, and the third generic is TKey, That is, the primary key type

 //Inherit IdentityDbContext<MyUser, MyRole, long>
    //These three generic types need to be added after the inheritance class here. The first two are added to use the customized attributes in our entities, and the last one is the type of the primary key.
    //If you don't add it, it will default to your entity class being IdentityUser, IdentityRole, rather than the class after the inherited class.
    public class MyIdentityDbContext : IdentityDbContext<MyUser, MyRole, long>
    {<!-- -->
        //In order for me to call this DBContext in other assemblies, use the DbContextOptions type
        public MyIdentityDbContext(DbContextOptions dbContextOptions): base(dbContextOptions)
        {<!-- -->
        
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {<!-- -->
            base.OnModelCreating(builder);
            builder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }

    }

In EF Core, we all operate the database by instantiating the DbContext class, but in the Identity framework, we are provided with two generic types, UserManager, and RoleManager Class, by calling the methods inside, you can operate the database more easily. T1 and T2 are respectively IdentityUser or a class that inherits the entity. The second generic is the IdentityRole type or a class that inherits the entity. Classes, for example, here are the MyUser entity class and the MyRole entity class.

5. Call the Migration tool for migration
Because I put the configuration and definition of Identity for the database in a separate assembly, it is convenient for other assemblies to call and can use different databases more flexibly. There is no need to rewrite OnConfiguring(DbContextOptionsBuilder optionsBuilder) in the DbContext class. The database to be called is hard-coded. Although it is more flexible, it will be more troublesome for us to migrate. We need to define a separate class in the assembly to help us migrate. The operation is as follows

Calling the Migration toolkit

Install-Package Microsoft.EntityFrameworkCore.Tools

Define a class that inherits the IDesignTimeDbContextFactory<T> interface. T refers to the DbContext you need to migrate. Define your reference to the database in this class. Whatever database you use, just call the package corresponding to the database.
 public class IdentityDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyIdentityDbContext>
    {<!-- -->
        public MyIdentityDbContext CreateDbContext(string[] args)
        {<!-- -->
            DbContextOptionsBuilder<MyIdentityDbContext> dbContextOptionsBuilder = new DbContextOptionsBuilder<MyIdentityDbContext>();
            //I am using the SqlServer database here, calling the Microsoft.EntityFrameworkCore.SqlServer package
            dbContextOptionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=true;TrustServerCertificate=true");
            MyIdentityDbContext myDBContext = new MyIdentityDbContext(dbContextOptionsBuilder.Options);
            return myDBContext;
        }
    }

The first premise is to declare a constructor with parameters of type DbContextOptions in DbContext, as shown in the fourth step above. In this way, you can use it here and register the service in other assemblies and write the configuration of the database, as shown in step 6 below.

//This is the constructor in the MyIdentityDbContext class in step 4, for demonstration only
public MyIdentityDbContext(DbContextOptions dbContextOptions): base(dbContextOptions)
{<!-- -->

}
//Register the DbContextOptions service to write the configuration of the database in other assemblies. This is only for demonstration, specific to the beginning of the sixth step of the code.
builder.Services.AddDbContext<MyIdentityDbContext>(options =>
 {<!-- -->
     options.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true");
 });
Set the assembly as a startup item, and then call the Add-Migration name, Update-Database and other commands in the package manager console (of course there are other commands that will not be described here)


After calling the Update-database command, if successful, go to the database to check whether the corresponding table has been generated.

6. Register the services required by Identity
Specifically as follows:

Note: The server configuration needs to be configured in Programs (as shown below) and cannot be configured in DbContext, otherwise an error will be reported for the subsequent service registration of the Identity framework.
builder.Services.AddDbContext(options =>
{
options.UseSqlServer(“Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true”);
});

 //Register Identity framework-----------------------
            //First register the DbContext written in other assemblies and write the configuration of the database, which is more flexible
            builder.Services.AddDbContext<MyIdentityDbContext>(options =>
            {<!-- -->
                options.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true");
            });
            //Register for data protection
            builder.Services.AddDataProtection();
            //builder.Services.AddIdentity(); This is generally used in MVC, rather than in front-end and back-end separation. It will provide some interface and other operations.
            //Here we need to add some changes to the login verification operation. For example, the entity I use to log in here is MyUser.
            builder.Services.AddIdentityCore<MyUser>(options =>
            {<!-- -->
                //If you use his original password, it will be more troublesome. His original password requires you to create it very complicated, so the password is simplified here, or you don’t need to set it.
                //The following are some operations on passwords, etc. You can check the documentation.
                //Set the number of incorrect password inputs here and write a few examples.
                options.Lockout.MaxFailedAccessAttempts = 5;//Wrong entries several times to lock
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(10);//Lock time
                //
                options.Password.RequireDigit = false;
                options.Password.RequireLowercase = false;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequiredLength = 6;
                /*
                PasswordResetTokenProvider: The reset password is the Token sent to you, which is the verification code.
                Notice:
                Here, when registering, set options.Tokens.PasswordResetTokenProvider to TokenOptions.DefaultEmailProvider, and the returned value is the value.
                options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
                The result of setting this default value is: 403797
                The result of not setting this value: CfDJ8LYtD6oWUe1GqPBiQxTKiPC1v0835ryQmJ8kuK86CKme6Ik6FDhmfY1uAIf01HpxshNC0/Rv4FWbwf3PNE9TYQqtYt1lX8JHwJ1ekAk6b0H6qi6gAViIMxOUFwhy4c5hMc8Fziw 9r2plFzfnEwok3ps8TJaqyH + WVcNp9O4rsBvzBgg9g8dnFgC5qFi/Jvmfeg==
                When this value is not set, it is generally used when a link needs to be generated.
                 */
                options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
                options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
            });
            //Next establish a relationship with the entity
            //Parameters: Login verification, role permission verification, service collection
            IdentityBuilder iBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
            //Connect entities with DbContext: iBuilder.AddEntityFrameworkStores<MyIdentityDbContext>()
            //iBuilder.AddUserManager<UserManager<MyUser>>(): Add login verification management
            //iBuilder.AddRoleManager<RoleManager<MyRole>>(): Add role permission management
            //Add services to the UserManager and RoleManager classes here for our convenience
            iBuilder.AddEntityFrameworkStores<MyIdentityDbContext>().AddDefaultTokenProviders().AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>();
            //Look at the above line of code. We knew before that we can directly instantiate DbContext to call the entities inside to complete the operation, but we don’t need to do this here.
            //Why .AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>(); is called later is because we can use the UserManager and UserManager classes provided by the Identity framework to operate the corresponding entities
            //----------------------------------------

7. Use UserManager and RoleManager to manipulate table data
The following is an example. When using the specific method, just check the documentation or go to the class to see it. The English meaning is the literal meaning.

[Route("api/[controller]/[action]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {<!-- -->
        //Inject the required classes, there is no need to use DbContext here
        private readonly UserManager<MyUser> userManager;
        private readonly RoleManager<MyRole> roleManager;
        private readonly IHostEnvironment hostEn;
        public ValuesController(RoleManager<MyRole> roleManager, UserManager<MyUser> userManager, IHostEnvironment hostEn)
        {<!-- -->
            this.userManager = userManager;
            this.roleManager = roleManager;
            this.hostEn = hostEn;
        }
        [HttpPost]
        public async Task<ActionResult<ObjectResult>> Test()
        {<!-- -->
            //Check if there is an admin role, if not, add it
            bool isHas = await this.roleManager.RoleExistsAsync("admin");
            if (!isHas)
            {<!-- -->
                MyRole myRole = new MyRole();
                myRole.Name = "admin";
                myRole.NormalizedName = "admin";
                myRole.ConcurrencyStamp = "1";

                //Add it in, create a new line and add it in
                IdentityResult result = await this.roleManager.CreateAsync(myRole);//Create role
                //Return an error if not successful
                if (!result.Succeeded)
                {<!-- -->
                    //BadResult is a method in the Controller parent class, and its return value is inherited from ObjectResult
                    // Logically speaking, error information should not be fed back. Log records should be used, and error feedback should be a string.
                    return BadRequest(result.Errors);
                }
            }
            //Check if there is a user pengmingxing, if so, return it, if not, create it, and its role is admin
            MyUser user = await this.userManager.FindByNameAsync("pengmingxing");//Find the corresponding user based on the user name
            if (user == null)
            {<!-- -->
                MyUser myUser = new MyUser();
                myUser.UserName = "pengmingxing";
                myUser.Email = "[email protected]";
                myUser.EmailConfirmed = true; //Entering password requires email verification
                myUser.PhoneNumber = "88888888888";
                myUser.WeiChart = "88888888";
                
                //The CreateAsync method used here to create a user does not need to set a password, such as mobile phone verification code login.
                //IdentityResult result = await this.userManager.CreateAsync(myUser);//Create user
                //If you don't set a password, you can use another one to rewrite it
                IdentityResult result = await this.userManager.CreateAsync(myUser, "888888");//Create user
                if (!result.Succeeded)
                {<!-- -->
                    return BadRequest(result.Errors);
                }

                //Bind this user with the admin role
                result = await this.userManager.AddToRoleAsync(myUser, "admin");//Add the user to the role
                if (!result.Succeeded)
                {<!-- -->
                    return BadRequest(result.Errors);
                }
            }
            else
            {<!-- -->
                //Determine whether the user is a certain role, if not, create it
                bool isRole = await userManager.IsInRoleAsync(user, "admin");
                if (!isRole)
                {<!-- -->
                    //Bind this user with the admin role
                    IdentityResult result = await this.userManager.AddToRoleAsync(user, "admin");//Add the user to the role
                    if (!result.Succeeded)
                    {<!-- -->
                        return BadRequest(result.Errors);
                    }
                }
            }
            return Ok();
        }

        [HttpPost]
        public async Task<ActionResult<string>> CheckUserPassword(string userName, string password)
        {<!-- -->
            //Check if there is a corresponding user
            MyUser user = await userManager.FindByNameAsync(userName);
            if (user == null)
            {<!-- -->
                //Some people will maliciously hack the website, so in a formal environment it is best not to have a username and not give any specific information.
                if (hostEn.IsDevelopment())
                {<!-- -->
                    return BadRequest("Username does not exist");
                }
                else
                {<!-- -->
                    return BadRequest();
                }
            }

            //First determine whether the user has been blocked or locked
            bool isLock = await userManager.IsLockedOutAsync(user);
            if(isLock)
            {<!-- -->
                return BadRequest($"This user has been locked, the lock end time is {user.LockoutEnd}");
            }
           
            //If the username exists, make sure the password is correct.
            bool isTrue = await userManager.CheckPasswordAsync(user, password);
            if(isTrue)
            {<!-- -->
                //If the login is successful, delete the recorded login failure data
                await userManager.ResetAccessFailedCountAsync(user);
                return Ok("Login successful");
            }
            else
            {<!-- -->
                //Record the input incorrect data
                await userManager.AccessFailedAsync(user);
                //Get the number of login failures
                int failNum = await userManager.GetAccessFailedCountAsync(user);
                //userManager.GetAccessFailedCountAsync(user); This method records the number of errors. If the number of errors exceeds a fixed number, the user will be locked.
                //You can set it when defining it earlier, you can also use the default value, or you can flexibly use the methods inside to set the lock time.

                return BadRequest("Incorrect username or password");
            }
        }

        [HttpPost]
        public async Task<ActionResult<string>> SendPasswordToken(string userName)
        {<!-- -->
            //Find the user
            MyUser user = await userManager.FindByNameAsync(userName);
            if (user == null)
            {<!-- -->
                if (hostEn.IsDevelopment())
                {<!-- -->
                    return BadRequest("Username does not exist");
                }
                return BadRequest();
            }

            //Get Token
            string token = await userManager.GeneratePasswordResetTokenAsync(user);
            //The Token will not be sent here, but written directly on the console.
            /*
            Notice:
            Here, when registering, set options.Tokens.PasswordResetTokenProvider to TokenOptions.DefaultEmailProvider, and the returned value is the value.
            options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
            The result of setting this default value is: 403797
            The result of not setting this value: CfDJ8LYtD6oWUe1GqPBiQxTKiPC1v0835ryQmJ8kuK86CKme6Ik6FDhmfY1uAIf01HpxshNC0/Rv4FWbwf3PNE9TYQqtYt1lX8JHwJ1ekAk6b0H6qi6gAViIMxOUFwhy4c5hMc8Fziw 9r2plFzfnEwok3ps8TJaqyH + WVcNp9O4rsBvzBgg9g8dnFgC5qFi/Jvmfeg==
            The second case below is generally used when a connection needs to be generated.
             */
            Console.WriteLine(token);

            return Ok();
        }

        /// <summary>
        /// Obtain the generated Token and then change the password
        /// I don’t know which table this Token is stored in. It may be stored in the field of the corresponding table data and then encrypted to prevent some people from stealing it.
        /// Or there is a space inside it that is specially saved for him.
        /// </summary>
        /// <param name="myUser"></param>
        /// <param name="token"></param>
        /// <param name="newPassword"></param>
        /// <returns></returns>
        [HttpPut]
        public async Task<ActionResult<string>> ResetPasswordByToken(string myUser, string token, string newPassword)
        {<!-- -->
            MyUser user = await userManager.FindByNameAsync(myUser);
            if (user == null)
            {<!-- -->
                if (hostEn.IsDevelopment())
                {<!-- -->
                    return BadRequest("Username does not exist");
                }
                return BadRequest();
            }

            //Reset username
            IdentityResult result = await userManager.ResetPasswordAsync(user, token, newPassword);
            if (result.Succeeded)
            {<!-- -->
                //Clear the previous incorrect password records
                await userManager.ResetAccessFailedCountAsync(user);
                return Ok("Password changed successfully");
            }
            else
            {<!-- -->
                //Record if failed
                await userManager.AccessFailedAsync(user);
                return BadRequest("Password modification failed");
            }
        }
    }