X

[Day 1] Easy custom Identity in ASP.NET Core 3.1 login page

Dung Do Tien Jul 31 2020 2213
Custome Identity in ASP.NET Core 3.1 helps users login to your web page very easily. We also can manage users, change passwords, update profile data, manage roles, claims, tokens, email confirmation, and more.

1. Set up the environment.

To start we need Visual Studio 2019 and MsSql server 18. You can use other RDBMS  like MySQL, PostgreSQL…
In this article, I will use ASP.NET Core 3.1 but you can use version 2.2 or 3.0 or maybe above 3.1 it is still working fine.
* notice: With MsSql server 18, you can use the lower version

2. Create Database

To check login account user info whether or not it exists we need a database to store the user account info.
I will create a database EmployeeManager and Employee table as below:

CREATE DATABASE EmployeeManager 
GO
USE EmployeeManager
GO
CREATE TABLE [Member](
    [Id] [int] IDENTITY(1,1) PRIMARY KEY NOT NULL,
    [PassWord] [varchar](100) NULL,
    [FullName] [nvarchar](100) NULL,
    [Email] [varchar](50) NULL,
    [CreatedDate] [datetime] NULL,
    [Avatar] [nvarchar](1000) NULL
)

GO
INSERT [dbo].[Member] ([PassWord], [FullName], [Email], [CreatedDate], [Avatar]) VALUES (N'e10adc3949ba59abbe56e057f20f883e', N'Quiz Developer', N'quizdev@gmail.com', CAST(N'2020-07-26T10:10:00.000' AS DateTime), NULL)

3. Create the project and login form.

Open Visual Studio 2019 and press Ctrl+Shift+N to create a new project.

Select the ASP.NET Core Web Application project type and click the Next button.

Enter your project name and click the Create button.

Select version of Asp.Net Core, in this example I will choose version latest 3.1 and select project type as Web Application and click Create button.

Now we need to create a login form. I will use Bootstrap to create forms faster. Please don’t forget Bootstrap is available when you create a project to finish.

And this is my login form:

4. Connect database and get account information

In this step, we need to create a function to connect the database and get account info by email & password.  This function is used to check if account information exists or not.

Step 1: Create interface help connect to database

We need to create a IDBHelper interface to support connecting to databases.

public interface IDbHelper
{
    Task<bool> ExecuteNonQuery(string query, List<SqlParameter> parameters, DbHelperEnum type);

    Task<T> ExecuteScalarFunction<T>(string query, List<SqlParameter> parameters, DbHelperEnum type, string outParams);

    Task<IEnumerable<T>> ExecuteToTableAsync<T>(string query, List<SqlParameter> parameters, DbHelperEnum type) where T : class;
}

In this interface we have three methods:

ExecuteNonQuery: Use for Insert/Update/Delete.

ExecuteScalarFunction: Get by id and return a single record.

ExecuteToTableAsync: Get and return many records.

Step 2: Implement IDBHelper interface

I will create a class with name DBHelper to implement the IDBHelper interface, look like as code below:

public class DbHelper: IDbHelper
{
    private SqlConnection _con;
    private SqlCommand _cmd;
    private SqlDataAdapter _adapter;
    private readonly int _connectDBTimeOut = 120;

    private static string _connectionString = "";

    public DbHelper()
    {
        _connectionString = AppSettings.Instance.GetConnection(Const.ConnectionString);
    }

    public async Task<bool> ExecuteNonQuery(string query, List<SqlParameter> parameters, DbHelperEnum type)
    {
        using (var con = new SqlConnection(_connectionString))
        {
            using (var cmd = new SqlCommand(query, con))
            {
                await con.OpenAsync();
                cmd.Connection = con;
                cmd.CommandType = type == DbHelperEnum.StoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
                cmd.CommandText = query;
                cmd.CommandTimeout = _connectDBTimeOut;

                if (parameters != null)
                    cmd.Parameters.AddRange(parameters.ToArray());

                int result = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                con.Dispose();

                if (con.State == ConnectionState.Open)
                    con.Close();

                return result > 0;
            }
        }
    }

    public async Task<T> ExecuteScalarFunction<T>(string query, List<SqlParameter> parameters, DbHelperEnum type, string outParams)
    {
        using (var con = new SqlConnection(_connectionString))
        {
            using (var cmd = new SqlCommand(query, con))
            {
                await con.OpenAsync();
                cmd.Connection = con;
                cmd.Parameters.Clear();
                cmd.CommandType = type == DbHelperEnum.StoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
                cmd.CommandText = query;
                cmd.CommandTimeout = _connectDBTimeOut;

                if (parameters != null)
                    cmd.Parameters.AddRange(parameters.ToArray());
                SqlParameter returnValue = cmd.Parameters.Add(new SqlParameter(outParams, 0));
                returnValue.Direction = ParameterDirection.Output;

                await cmd.ExecuteNonQueryAsync();

                con.Dispose();
                if (con.State == ConnectionState.Open) con.Close();

                return (T)returnValue.Value;
            }
        }
    }

    public async Task<IEnumerable<T>> ExecuteToTableAsync<T>(string query, List<SqlParameter> parameters, DbHelperEnum type) where T : class
    {
        try
        {
            IEnumerable<T> result = new List<T>();

            using (var con = new SqlConnection(_connectionString))
            {
                using (var cmd = new SqlCommand(query, con))
                {
                    Console.WriteLine("Open connecting ......");
                    var watch = System.Diagnostics.Stopwatch.StartNew();
                    await con.OpenAsync();
                    watch.Stop();
                    Console.WriteLine(watch.ElapsedMilliseconds);
                    Console.WriteLine("Open connected");
                    cmd.Parameters.Clear();
                    cmd.CommandType = type == DbHelperEnum.StoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
                    cmd.CommandTimeout = _connectDBTimeOut;

                    if (parameters != null) cmd.Parameters.AddRange(parameters.ToArray());

                    using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
                    {
                        if (reader.HasRows)
                        {
                            result = await Mapper<T>(reader);
                            reader.Close();
                        }
                    }

                    if (con.State == ConnectionState.Open) con.Close();
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    #region Private func

    public async Task<IList<T>> Mapper<T>(SqlDataReader reader, bool close = true) where T : class
    {
        try
        {
            IList<T> entities = new List<T>();

            if (reader != null && reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    T item = default(T);
                    if (item == null)
                        item = Activator.CreateInstance<T>();
                    Mapper(reader, item);
                    entities.Add(item);
                }

                if (close)
                {
                    reader.Close();
                }
            }

            return entities;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    private bool Mapper<T>(IDataRecord reader, T entity) where T : class
    {
        Type type = typeof(T);

        if (entity != null)
        {
            for (var i = 0; i < reader.FieldCount; i++)
            {
                var fieldName = reader.GetName(i);
                try
                {
                    var propertyInfo = type.GetProperties().FirstOrDefault(info => info.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase));

                    if (propertyInfo != null)
                    {
                        var value = reader[i];
                        if ((reader[i] != null) && (reader[i] != DBNull.Value))
                        {
                            propertyInfo.SetValue(entity, reader[i], null);
                        }
                        else
                        {
                            if (propertyInfo.PropertyType == typeof(System.DateTime) ||
                                propertyInfo.PropertyType == typeof(System.DateTime?))
                            {
                                propertyInfo.SetValue(entity, System.DateTime.MinValue, null);
                            }
                            else if (propertyInfo.PropertyType == typeof(string))
                            {
                                propertyInfo.SetValue(entity, string.Empty, null);
                            }
                            else if (propertyInfo.PropertyType == typeof(bool) ||
                                propertyInfo.PropertyType == typeof(bool?))
                            {
                                propertyInfo.SetValue(entity, false, null);
                            }
                            else if (propertyInfo.PropertyType == typeof(decimal) ||
                                propertyInfo.PropertyType == typeof(decimal?))
                            {
                                propertyInfo.SetValue(entity, decimal.Zero, null);
                            }
                            else if (propertyInfo.PropertyType == typeof(double) ||
                            propertyInfo.PropertyType == typeof(double?))
                            {
                                propertyInfo.SetValue(entity, double.Parse("0"), null);
                            }
                            else if (propertyInfo.PropertyType == typeof(float) ||
                       propertyInfo.PropertyType == typeof(float?))
                            {
                                propertyInfo.SetValue(entity, 0, null);
                            }
                            else if (propertyInfo.PropertyType == typeof(short) ||
                       propertyInfo.PropertyType == typeof(short?))
                            {
                                propertyInfo.SetValue(entity, short.Parse("0"), null);
                            }
                            else if (propertyInfo.PropertyType == typeof(long) ||
                       propertyInfo.PropertyType == typeof(long?))
                            {
                                propertyInfo.SetValue(entity, long.Parse("0"), null);
                            }
                            else if (propertyInfo.PropertyType == typeof(int) ||
                       propertyInfo.PropertyType == typeof(int?))
                            {
                                propertyInfo.SetValue(entity, int.Parse("0"), null);
                            }
                            else
                            {
                                propertyInfo.SetValue(entity, 0, null);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    #endregion
}

* NoteAppSettings.Instance.GetConnection(Const.ConnectionString): This is a line code to help read values from an appsetting.json file. To understand & copy code you can refer below article for detail:

Get value from appsettings json c# .net core.

Step 3: Create model & business function to get account info

- Create a store procedure to get account information from the database: 

CREATE PROCEDURE [dbo].[Employee_GetByEmail]
(
   @Email VARCHAR(50)
)
AS
BEGIN
   SELECT  [Id] ,[PassWord] ,[FullName] ,[Email] ,[CreatedDate] ,[Avatar]
   FROM Member WHERE Email = @Email
END

GO

CREATE PROCEDURE [dbo].[Employee_GetById]
(
   @Id INTEGER
)
AS
BEGIN
   SELECT  [Id] ,[PassWord] ,[FullName] ,[Email] ,[CreatedDate] ,[Avatar]
   FROM Member WHERE Id = @Id
END

- Create the Employee entity

public class EmployeeModel
{
    public int Id { get; set; }
    public string PassWord { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public DateTime CreatedDate { get; set; }
    public string Avatar { get; set; }
}

- Create EmployeeDal class to get account info

public interface IEmployeeDal
{
    Task<EmployeeModel> GetById(int id);
    Task<EmployeeModel> GetByEmail(string email);
}
    
    
public class EmployeeDal : IEmployeeDal
{
    private IDbHelper _db;
    public EmployeeDal(IDbHelper db)
    {
        this._db = db;
    }

    public async Task<EmployeeModel> GetById(int id)
    {
        try
        {
            string sp = "Employee_GetById";
            List<SqlParameter> parms = new List<SqlParameter>();
            parms.Add(new SqlParameter("Id", id));
            var data = await _db.ExecuteToTableAsync<EmployeeModel>(sp, parms, DbHelperEnum.StoredProcedure);
            return data != null ? data.FirstOrDefault() : null;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }
    }

    public async Task<EmployeeModel> GetByEmail(string email)
    {
        try
        {
            string sp = "Employee_GetByEmail";
            List<SqlParameter> parms = new List<SqlParameter>();
            parms.Add(new SqlParameter("Email", email));
            var data = await _db.ExecuteToTableAsync<EmployeeModel>(sp, parms, DbHelperEnum.StoredProcedure);
            return data != null ? data.FirstOrDefault() : null;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }
    }
}

5. Configure Identity services

Open file Startup.cs you will see two methods. They are ConfigureServices() and Configure().

+ ConfigureServices(): This method gets called by the runtime. Use this method to add services to the container.

+ Configure(): This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

To declare Identity, in ConfigureServices() method we will add service in this method.
I will add configure for Identity authorization and cookie as below: 

Step 1: Configure Identity options

services.Configure<IdentityOptions>(options =>
{
    // Password settings
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 6;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireLowercase = false;

    // Lockout settings
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
    options.Lockout.MaxFailedAccessAttempts = 10;

    // User settings
    options.User.RequireUniqueEmail = true;
});

This config is some option for user info with default value includes:
- Setting a valid password for signup.
- Lockout settings when login wrong info many times.
- Setting user info: rule for username and request a unique email.

services.ConfigureApplicationCookie(options =>
{
    // Cookie settings
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(90);

    options.LoginPath = "/";
    options.AccessDeniedPath = "/404";
    options.SlidingExpiration = true;

});

Step 3: Add authentication to the service container

In Asp.net core support login with Identity and JWT, This step will define which login type to use for login. CookieAuthenticationDefaults.AuthenticationScheme will return Cookies value, It says that user log in information will be stored in the cookie and using Identity type to log in.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();

Step 4: Listen to authentication to each request 

Now we need to enable Identity middleware to filter pipeline requests and responses.
To enable Identity, we need to call UseAuthentication() middleware in Configure() of StartUp.cs file.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    .......
    app.UseAuthentication();
    ......
}

6. Custom Identity in Asp.net Core

Inside of identity in Asp.net Core we have two main parts we have to override, they are User information and Role management.

- User information: This object helps manipulation with user login such as login, logout, registration, check exist … Asp.net core provided three interfaces to help override all functions related to user information: IUserEmailStore, IUserPasswordStore and IUserLoginStore.

- Role management: This object helps manage all roles of users. To override role managers you need to implement the IRoleStore interface.

Step 1: Create two model objects for User information and Role management.

I will create two model objects: AppUser and AppRole. Two objects are very important, they will be used throughout to the follow of custom Identity. They need to inherit from IIdentity interface. 

- AppUser: Contain all information about user login.

public class AppUser : IIdentity
{
    public virtual int Id { get; set; }
    private string _userName;
    private string _email;

    public virtual string UserName
    {
        set => _userName = value;
        get => _userName?.ToLower();
    }

    public virtual string Email
    {
        set => _email = value;
        get => _email?.ToLower();
    }

    public string PassWord { get; set; }
    public string FullName { get; set; }
    public DateTime CreatedDate { get; set; }
    public string Avatar { get; set; }

    public string AuthenticationType { get; set; }

    public bool IsAuthenticated { get; set; }

    public string Name { get; set; }
}

- AppRole: Contain all role information of user login.

In this article, we will not do anything related to the role of the user so I do not create any custom property, all properties below are inherited from IIdentity interface.

public class AppRole : IIdentity
{
    public string AuthenticationType { get; set; }

    public bool IsAuthenticated { get; set; }

    public string Name { get; set; }
}

Step 2: Create two services to implement all interfaces of User information and Role management.

- Create UserStoreAppService class to implement three interfaces of User information.

public class UserStoreAppService : IUserEmailStore<AppUser>, IUserPasswordStore<AppUser>, IUserLoginStore<AppUser>, IDisposable
{
   //…. All methods implement from 3 interfaces here.
}

After inheriting all methods of three interfaces, You will see that we have many methods we need to inherit. But we only care about the five methods below, Those 5 methods are used for login follow: 

- FindByEmailAsync(): Help find user login by email.

- FindByNameAsync(): Help find user login by username or email.

- GetPasswordHashAsync(): Help get the password of the user from the database.

- GetUserIdAsync(): Help get the id of the user from the database.

- GetUserNameAsync(): Help get the username or email of the user from the database.

- FindByIdAsync(): Help find user login by id.

Below is full code you need to do:

public class UserStoreAppService : IUserEmailStore<AppUser>, IUserPasswordStore<AppUser>, IUserLoginStore<AppUser>, IDisposable
{
    private IEmployeeDal _employeeDal;
    public UserStoreAppService(IEmployeeDal employeeDal)
    {
        _employeeDal = employeeDal;
    }

    public Task AddLoginAsync(AppUser user, UserLoginInfo login, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> CreateAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> DeleteAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
       
    }

    public async Task<AppUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
    {
        if (normalizedEmail == null) throw new ArgumentNullException(nameof(normalizedEmail));
        var employee = await _employeeDal.GetByEmail(normalizedEmail);
        if (employee != null)
        {
            var appUser = new AppUser()
            {
                Id = employee.Id,
                PassWord = employee.PassWord,
                Email = employee.Email,
                FullName = employee.FullName,
                Avatar = employee.Avatar
            };
            return appUser;
        }
        return null;
    }

    public Task<AppUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
    {
        if (!string.IsNullOrEmpty(userId))
                userId = userId.ToLower();
            cancellationToken.ThrowIfCancellationRequested();
            if (userId == null) throw new ArgumentNullException(nameof(userId));
            var employee = await _employeeDal.GetById(Convert.ToInt32(userId));
            if (employee != null)
            {
                var appUser = new AppUser()
                {
                    Id = employee.Id,
                    PassWord = employee.PassWord,
                    Email = employee.Email,
                    FullName = employee.FullName,
                    Avatar = employee.Avatar
                };
                return appUser;
            }
            return null;
    }

    public Task<AppUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public async Task<AppUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var employee = await _employeeDal.GetByEmail(normalizedUserName.ToLower());
        if (employee != null)
        {
            var appUser = new AppUser()
            {
                Id = employee.Id,
                PassWord = employee.PassWord,
                Email = employee.Email,
                FullName = employee.FullName,
                Avatar = employee.Avatar
            };
            return appUser;
        }
        return null;
    }

    public Task<string> GetEmailAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<bool> GetEmailConfirmedAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IList<UserLoginInfo>> GetLoginsAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetNormalizedEmailAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetNormalizedUserNameAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetPasswordHashAsync(AppUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return Task.FromResult(user.PassWord);
    }

    public Task<string> GetUserIdAsync(AppUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (user == null) throw new ArgumentNullException(nameof(user));

        return Task.FromResult(user.Id.ToString());
    }

    public Task<string> GetUserNameAsync(AppUser user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return Task.FromResult(user.Email);
    }

    public Task<bool> HasPasswordAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task RemoveLoginAsync(AppUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetEmailAsync(AppUser user, string email, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetEmailConfirmedAsync(AppUser user, bool confirmed, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetNormalizedEmailAsync(AppUser user, string normalizedEmail, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetNormalizedUserNameAsync(AppUser user, string normalizedName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetPasswordHashAsync(AppUser user, string passwordHash, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetUserNameAsync(AppUser user, string userName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> UpdateAsync(AppUser user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

}

- Create RoleAppService class to implement Role management.

Because in the scope of this article, I only talk about information related to log in so I only implement IRoleStore interface and no code anymore inside of the implementation class.

public class RoleAppService : IRoleStore<AppRole>
{
    private bool disposed;
    public Task<IdentityResult> CreateAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> DeleteAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        disposed = true;
    }

    public Task<AppRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<AppRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetNormalizedRoleNameAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetRoleIdAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetRoleNameAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetNormalizedRoleNameAsync(AppRole role, string normalizedName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetRoleNameAsync(AppRole role, string roleName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> UpdateAsync(AppRole role, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

Step 3: Implement PasswordHasher class to help compare passwords when login.

This is an important step because it helps you compare passwords from the database with password user provided. If they are matched, the method will return Success and opposite return Failed.

Now I will create PasswordHasherOverride class to inherit PasswordHasher class, It looks like the below code :

public class PasswordHasherOverride<TUser> : PasswordHasher<TUser> where TUser : AppUser
{
    public override PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
    {
        if (hashedPassword == null) { throw new ArgumentNullException(nameof(hashedPassword)); }
        if (providedPassword == null) { throw new ArgumentNullException(nameof(providedPassword)); }

        var hashedProvidedPassword = Utils.Utils.GetMd5x2(providedPassword);

        if (hashedPassword != hashedProvidedPassword)
        {
            return PasswordVerificationResult.Failed;
        }
        else
        {
            return PasswordVerificationResult.Success;
        }
    }
}

Utils.Utils.GetMd5x2(): This function I created to encrypt the password. You can create function as below:

public class Utils
{
    public static string GetMd5x2(string str)
    {
        MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
        byte[] bytes = Encoding.UTF8.GetBytes(str);
        bytes = provider.ComputeHash(bytes);
        StringBuilder builder = new StringBuilder();
        foreach (byte num in bytes)
        {
            builder.Append(num.ToString("x2").ToLower());
        }
        return builder.ToString();
    }
}

Step 4: Register custom Identity to the service container.

After creating two custom service classes implemented for User management and Role management we need to register them to the service container of application.

Inside ConfigureServices() method of Startup.cs class, you can put code as below :

First, You HAVE TO add below code above code of step 1 & step 2 of 5. If not code custom config of step 1 & step 2 of 5 will not accept and can't rewrite.

// Adds the default identity system configuration for the specified User and Role
services.AddIdentity<AppUser, AppRole>().AddDefaultTokenProviders();

Second, add custom Identity class to the service container.

// Add custom User and Role information
services.AddTransient<IUserStore<AppUser>, UserStoreAppService>();
services.AddTransient<IRoleStore<AppRole>, RoleAppService>();
services.AddScoped<IPasswordHasher<AppUser>, PasswordHasherOverride<AppUser>>();

// Add all custom service class business to DI container
services.AddScoped<IEmployeeDal, EmployeeDal>();
services.AddScoped<IDbHelper, DbHelper>();

7. Handle and process view and controller for Login form

Okay, we are almost done,  all business code for custom Identity in Asp.net core is done, now we only need to process something for view and controller such as validate the form, check existing account, etc ...

Step 1: Design and validate the required input form.

-  We need to create a view model to transfer data user input from view to controller and opposite.

public class LoginViewModel
{
    [Display(Name = "Email")]
    [Required(ErrorMessage = "Please enter your email.")]
    [RegularExpression(@"^[\w+][\w\.\-]+@[\w\-]+(\.\w{2,4})+$|^\d{4}(\-)?\d{6}$|^91\-?\d{4}\-?\d{6}$", ErrorMessage = "Email invalid format.")]
    public string Email { get; set; }

    [Display(Name = "Password")]
    [Required(ErrorMessage = "Please enter password.")]
    [DataType(DataType.Password)]
    [UIHint("stringPassword")]
    [RegularExpression(@"[^<>]*", ErrorMessage = "The password format is incorrect.")]
    public string Password { get; set; }

}

- Design view & handle validate form

@model EmployeeManager.Bsl.Model.LoginViewModel
@{
    Layout = "~/Pages/Shared/_Layout.cshtml";
}
<div id="logreg-forms">
    <form class="form-signin" method="post" asp-action="Login">
        <h1 class="h3 mb-3 font-weight-normal" style="text-align: center"> Sign in</h1>
        <div class="social-login">
            <button class="btn facebook-btn social-btn" type="button"><span><i class="fab fa-facebook-f"></i> Sign in with Facebook</span> </button>
            <button class="btn google-btn social-btn" type="button"><span><i class="fab fa-google-plus-g"></i> Sign in with Google+</span> </button>
        </div>
        <p style="text-align:center"> OR  </p>
        <div class="red-msg">@Html.ValidationSummary(false)</div>
        <input type="email" asp-for="Email" id="inputEmail" class="form-control" placeholder="Email address" autofocus="">
        <span class="red-msg" asp-validation-for="Email"></span>

        <input type="password" asp-for="Password" id="inputPassword" class="form-control" placeholder="Password">
        <span class="red-msg" asp-validation-for="Password"></span>

        <button class="btn btn-success btn-block" type="submit"><i class="fas fa-sign-in-alt"></i> Sign in</button>
        <a href="#" id="forgot_pswd">Forgot password?</a>
        <hr>
        <!-- <p>Don't have an account!</p>  -->


        <button class="btn btn-primary btn-block" type="button" id="btn-signup"><i class="fas fa-user-plus"></i> Sign up New Account</button>
    </form>
</div>

<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

I used asp-validation-for to display message validate error for each input control. And I also created a form with the POST method and called the Login method of the Employee controller.

@Html.ValidationSummary(false): Help display other messages error from the controller.

Step 2: Handle action inside the controller to submit the form.

I created the Employee controller and content some method :

First, we need to create a constructor for the Employee controller:

private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
public EmployeeController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager)
{
    this._userManager = userManager;
    this._signInManager = signInManager;
}

In that :
UserManager: Provides the APIs for managing users in a persistence store.
SignInManager: Provides the APIs for user sign in.

- Create  login page view

[HttpGet]
public IActionResult Login()
{
    return View();
}

- Create the Welcome page after login successfully.

[HttpGet]
[Authorize]
public IActionResult Welcome()
{
    return View();
}

- Create the Logout method that helps signout users.

public async Task<IActionResult> Logout()
{
    await _signInManager.SignOutAsync().ConfigureAwait(false);
    return Redirect("/");
}

- And the last method most important is process login when the user submits the login form.

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    model.Email = model.Email.Trim();
    var appUser = await _userManager.FindByEmailAsync(model.Email).ConfigureAwait(false);
    if (appUser == null)
    {
        ModelState.AddModelError("FileNameValidation", "Account does not exist.");
        return View(model);
    }

    var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, true, lockoutOnFailure: false).ConfigureAwait(false);
    if (result.Succeeded)
    {
        return Redirect("/Employee/Welcome");
    }

    ModelState.AddModelError("FileNameValidation", "Password does not match.");
    return View(model);
}

*Note: method PasswordSignInAsync() will call to PasswordHasherOverride class to compare passwords.

- Create Welcome Html page

@{
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

@if (User.Identity.IsAuthenticated)
{

    <h6> Welcome <b style="color: red; font-size: 22px;">@User.Identity.Name</b></h6>
    <p>
        <a href="/employee/logout" style="text-decoration: underline;">Logout</a>
    </p>
}

Step 3: See the result.

 

 

8. Custom Claim Principal Identity

By default, we only get email or username of user login through User.Identity.Name, And now we can get more information from users by custom claim principal identity.

Step 1. Implement Claim Identity

To custom claim principal identity we need to implement UserClaimsPrincipalFactory class. I will create the CustomClaimsPrincipal class to custom claim principal identity. Look like as below code :

public class CustomClaimsPrincipal : UserClaimsPrincipalFactory<AppUser>
{
    private readonly UserManager<AppUser> _userManger;
    public CustomClaimsPrincipal(UserManager<AppUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
    {
        _userManger = userManager;
    }

    public override async Task<ClaimsPrincipal> CreateAsync(AppUser user)
    {
        try
        {
            var principal = await base.CreateAsync(user);
            ((ClaimsIdentity)principal.Identity).AddClaims(new[]
            {
                new Claim("Id", user.Id.ToString()),
                new Claim("Email", user.Email),
                new Claim("DisplayName", user.FullName ?? user.Email),
                new Claim("Avatar", user.Avatar ?? string.Empty),
            });
            return principal;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Step 2: register custom class to the service container

Finally, We need to register this class to the service container, inside ConfigureServices() method of Startup.cs file :

services.AddScoped<IUserClaimsPrincipalFactory<AppUser>, CustomClaimsPrincipal>();

Step 3: Get more information from Claim

Change some code in the Welcome page as below :

@page
@using System.Security.Claims
@{
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

@if (User.Identity.IsAuthenticated)
{

    ClaimsPrincipal user = User;
    var claimId = user.Claims.FirstOrDefault(x => x.Type == "Id");
    var claimEmail = user.Claims.FirstOrDefault(x => x.Type == "Email");
    var claimDisplayName = user.Claims.FirstOrDefault(x => x.Type == "DisplayName");

    string userId = claimId != null ? claimId.Value : string.Empty;
    string userEmail = claimEmail != null ? claimEmail.Value : string.Empty;
    string userDisplayName = claimDisplayName != null ? claimDisplayName.Value : string.Empty;

    <h6> Welcome <b style="color: red; font-size: 22px;">@User.Identity.Name</b></h6>
    <ul>
        <li>Id : @userId</li>
        <li>Email : @userEmail</li>
        <li>Display name : @userDisplayName</li>
    </ul>
    <p>
        <a href="/employee/logout" style="text-decoration: underline;">Logout</a>
    </p>
}

And this is the result update :

9. Summary

In this article, I only want to guide you to custom authentication with Identity in Asp.net core. Please notice that I am using .net core 3.1 but I’m not using Razor Page, I am still using controller-view. You can use the razor page if you want.

Link download source code from Github here.

Happy code!!