DEV Community

Masui Masanori
Masui Masanori

Posted on

7

【.NET 5】【ASP.NET Core Identity】SignIn with custom user

Intro

In this time, I try signing in with custom user.
Because the default ASP.NET Core Identity user has so much properties and they are too much for me.

  • Id
  • UserName
  • Email
  • PasswordHash
  • EmailConfirmed
  • NormalizedUserName
  • NormalizedEmail
  • LockoutEnabled
  • AccessFailedCount
  • PhoneNumber
  • ConcurrencyStamp
  • SecurityStamp
  • LockoutEnd
  • TwoFactorEnabled
  • PhoneNumberConfirmed

I will use first four one.
To do this, I must do some things.

Environments

  • NLog.Web.AspNetCore ver.4.10.0
  • Npgsql.EntityFrameworkCore.PostgreSQL ver.5.0.2
  • Microsoft.EntityFrameworkCore ver.5.0.2
  • Microsoft.EntityFrameworkCore.Design ver.5.0.2
  • Newtonsoft.Json ver.12.0.3
  • Microsoft.AspNetCore.Mvc.NewtonsoftJson ver.5.0.2
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore

Samples

Create custom user

To create custom user, I inherit "IdentityUser".
And I added "Organization" and "LastUpdateDate".

ApplicationUser.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;

namespace ApprovementWorkflowSample.Applications
{
    // To use int as ID type, I inherited "IdentityUser<int>".
    public class ApplicationUser: IdentityUser<int>
    {
        [Key]
        [Column("id")]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public override int Id { get; set; }
        [Required]
        [Column("user_name")]
        public override string UserName { get; set; } = "";
        [Column("organization")]
        public string? Organization { get; set; }
        [Required]
        [Column("mail")]
        public override string Email { get; set; } = "";
        [Required]
        [Column("password")]
        public override string PasswordHash { get; set; } = "";
        [Required]
        [Column("last_update_date", TypeName = "timestamp with time zone")]
        public DateTime LastUpdateDate { get; set; }
        [NotMapped]
        public override bool EmailConfirmed { get; set; }
        [NotMapped]
        public override string NormalizedUserName {
            get
            {
                return UserName.ToUpper();
            }
            set { /* DO nothing*/ }
        }
        [NotMapped]
        public override string NormalizedEmail {
            get
            {
                return Email.ToUpper();
            }
            set { /* DO nothing*/ }
        }
        [NotMapped]
        public override bool LockoutEnabled { get; set; }
        [NotMapped]
        public override int AccessFailedCount { get; set; }
        [NotMapped]
        public override string? PhoneNumber { get; set; }
        [NotMapped]
        public override string? ConcurrencyStamp { get; set; }
        [NotMapped]
        public override string? SecurityStamp { get; set; }
        [NotMapped]
        public override DateTimeOffset? LockoutEnd { get; set; }
        [NotMapped]
        public override bool TwoFactorEnabled { get; set; }
        [NotMapped]
        public override bool PhoneNumberConfirmed { get; set; }     
        public void Update(ApplicationUser user)
        {
            UserName = user.UserName;
            Organization = user.Organization;
            Email = user.Email;
            PasswordHash = user.PasswordHash;
        }
        public void Update(string userName, string? organization,
            string email, string password)
        {
            UserName = userName;
            Organization = organization;
            Email = email;
            // set hashed password text to PasswordHash.
            PasswordHash = new PasswordHasher<ApplicationUser>()
                .HashPassword(this, password);
        }
        public string Validate()
        {
            if(string.IsNullOrEmpty(UserName))
            {
                return "UserName is required";
            }
            if(string.IsNullOrEmpty(Email))
            {
                return "E-Mail address is required";
            }
            if(string.IsNullOrEmpty(PasswordHash))
            {
                return "Password is required";
            }
            return "";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I create a Database and add "ApplicationUser" table like this.

Unique Constraint

By default, UserName is constrained to be unique.
In this sample, I also need constraining Email.

ApprovementWorkflowContext.cs

using ApprovementWorkflowSample.Applications;
using Microsoft.EntityFrameworkCore;

namespace ApprovementWorkflowSample.Models
{
    public class ApprovementWorkflowContext: DbContext
    {
...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ApplicationUser>()
                .Property(w => w.LastUpdateDate)
                .HasDefaultValueSql("CURRENT_TIMESTAMP");
            modelBuilder.Entity<ApplicationUser>()
                .HasIndex(u => u.Email)
                .IsUnique();                
        }
        public DbSet<ApplicationUser> ApplicationUsers => Set<ApplicationUser>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Role

This time, I won't add any special roles.
So I use default "IdentityRole".

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
...
namespace ApprovementWorkflowSample
{
    public class Startup
    {
...
        public void ConfigureServices(IServiceCollection services)
        {
...
            services.AddDbContext<ApprovementWorkflowContext>(options =>
                options.UseNpgsql(configuration["DbConnection"]));
            // THIS PROJECT CAUSES AN ERROR
            services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                .AddEntityFrameworkStores<ApprovementWorkflowContext>()
                .AddDefaultTokenProviders();
...
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

IdentityRole

When I use "IdentityUser<int>", I must use "IdentityRole<int>".
Or I will get an exception on runtime.

Stopped program because of exception System.ArgumentException: GenericArguments[1], 'Microsoft.AspNetCore.Identity.IdentityRole', on 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`4[TUser,TRole,TContext,TKey]' violates the constraint of type 'TRole'.
 ---> System.TypeLoadException: GenericArguments[1], 'Microsoft.AspNetCore.Identity.IdentityRole', on 'Microsoft.AspNetCore.Identity.UserStoreBase`8[TUser,TRole,TKey,TUserClaim,TUserRole,TUserLogin,TUserToken,TRoleClaim]' violates the constraint of type parameter 'TRole'.
...
Enter fullscreen mode Exit fullscreen mode

Add custom UserStore

This project has a problem.
Because some properties of "ApplicationUser" are null.

For avoiding NullReferenceException, I must add a custom UserStore.
Because I will sign in by password, it implement "IUserPasswordStore".

ApplicationUserStore.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using ApprovementWorkflowSample.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;

namespace ApprovementWorkflowSample.Applications
{
    public class ApplicationUserStore: IUserPasswordStore<ApplicationUser>
    {
        private readonly ILogger<ApplicationUserStore> logger;
        private readonly ApprovementWorkflowContext context;
        public ApplicationUserStore(ILogger<ApplicationUserStore> logger,
            ApprovementWorkflowContext context)
        {
            this.logger = logger;
            this.context = context;
        }
        public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            // validation
            string validationError = user.Validate();
            if(string.IsNullOrEmpty(validationError) == false)
            {
                return IdentityResult.Failed(new IdentityError { Description = validationError });
            }
            using(IDbContextTransaction transaction = context.Database.BeginTransaction())
            {
                if(await context.ApplicationUsers
                    .AnyAsync(u => u.Email == user.Email,
                    cancellationToken))
                {
                    return IdentityResult.Failed(new IdentityError { Description = "Your e-mail address is already used" });
                }
                var newUser = new ApplicationUser();
                newUser.Update(user);
                await context.ApplicationUsers.AddAsync(newUser, cancellationToken);
                await context.SaveChangesAsync(cancellationToken);
                await transaction.CommitAsync();
                return IdentityResult.Success;
            }
        }
        public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            ApplicationUser? target = await context.ApplicationUsers
                .FirstOrDefaultAsync(u => u.Id == user.Id,
                cancellationToken); 
            context.ApplicationUsers.Remove(target);
            await context.SaveChangesAsync(cancellationToken);
            return IdentityResult.Success;
        }
        public void Dispose() { /* do nothing */ }
        public async Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            if(int.TryParse(userId, out var id) == false)
            {
                return new ApplicationUser();
            }
            return await context.ApplicationUsers
                .FirstOrDefaultAsync(u => u.Id == id,
                cancellationToken);
        }
        public async Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            return await context.ApplicationUsers
                .FirstOrDefaultAsync(u => u.UserName.ToUpper() == normalizedUserName,
                cancellationToken);
        }
        public async Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return await Task.FromResult(user.NormalizedUserName);
        }
        public async Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return await Task.FromResult(user.PasswordHash);
        }
        public async Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return await Task.FromResult(user.Id.ToString());
        }
        public async Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return await Task.FromResult(user.UserName);
        }
        public async Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return await Task.FromResult(true);
        }
        public async Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
        {
            // do nothing
            await Task.Run(() => {});
        }
        public async Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
        {
            using(IDbContextTransaction transaction = context.Database.BeginTransaction())
            {
                ApplicationUser? target = await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.Id == user.Id,
                    cancellationToken);
                target.PasswordHash = passwordHash;
                // validation
                await context.SaveChangesAsync();
                await transaction.CommitAsync();
            }            
        }
        public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        {
            using(IDbContextTransaction transaction = context.Database.BeginTransaction())
            {
                ApplicationUser? target = await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.Id == user.Id,
                    cancellationToken);
                target.UserName = userName;
                // validation
                await context.SaveChangesAsync();
                await transaction.CommitAsync();
            }
        }
        public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            string validationError = user.Validate();
            if(string.IsNullOrEmpty(validationError) == false)
            {
                return IdentityResult.Failed(new IdentityError { Description = validationError });
            }
            using(IDbContextTransaction transaction = context.Database.BeginTransaction())
            {
                ApplicationUser? target = await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.Id == user.Id,
                    cancellationToken);
                // validation
                target.Update(user);
                await context.SaveChangesAsync(cancellationToken);
                await transaction.CommitAsync();
                return IdentityResult.Success;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Startup.cs

...
namespace ApprovementWorkflowSample
{
    public class Startup
    {
...
        public void ConfigureServices(IServiceCollection services)
        {
...
            // OK
            services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                .AddUserStore<ApplicationUserStore>()
                .AddEntityFrameworkStores<ApprovementWorkflowContext>()
                .AddDefaultTokenProviders();
...
Enter fullscreen mode Exit fullscreen mode

Resources

SignInManager

SignIn & SignOut

I can sign in and sign out through "SignInManager".

ApplicationUserService.cs

using System.Security.Claims;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;

namespace ApprovementWorkflowSample.Applications
{
    public class ApplicationUserService: IApplicationUserService
    {
        private readonly ILogger<ApplicationUsers> logger;
        private readonly IApplicationUsers applicationUsers;
        private readonly SignInManager<ApplicationUser> signInManager;

        public ApplicationUserService(ILogger<ApplicationUsers> logger,
            IApplicationUsers applicationUsers,
            SignInManager<ApplicationUser> signInManager)
        {
            this.logger = logger;
            this.applicationUsers = applicationUsers;
            this.signInManager = signInManager;
        }
        public async Task<bool> SignInAsync(string email, string password)
        {
            var target = await applicationUsers.GetByEmailAsync(email);
            if (target == null)
            {
                return false;
            }
            var result = await signInManager.PasswordSignInAsync(target, password, false, false);
            return result.Succeeded;
        }
        public async Task SignOutAsync()
        {
            await signInManager.SignOutAsync();
        }        
    }
}
Enter fullscreen mode Exit fullscreen mode

"PasswordSignInAsync" only can use "UserName" to sign in.
So if I want to sign in by e-mail, I must find user by e-mail first like above.

Create users

"SignInManager" also can create users.

ApplicationUserService.cs

...
namespace ApprovementWorkflowSample.Applications
{
    public class ApplicationUserService: IApplicationUserService
    {
...
        public async Task<IdentityResult> CreateAsync(string userName, string? organization, string email, string password)
        {
            var newUser = new ApplicationUser();
            newUser.Update(userName, organization, email, password);
            return await signInManager.UserManager.CreateAsync(newUser);
        }
...
Enter fullscreen mode Exit fullscreen mode

Get signed in user infomations

After signing in, I can get user infomations from HttpContext.

ApplicationUserService.cs

using System.Security.Claims;
using System;
using System.Threading.Tasks;
using ApprovementWorkflowSample.Applications.Dto;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;

namespace ApprovementWorkflowSample.Applications
{
    public class ApplicationUserService: IApplicationUserService
    {
...
        private readonly SignInManager<ApplicationUser> signInManager;
        private readonly IHttpContextAccessor httpContextAccessor;

        public ApplicationUserService(ILogger<ApplicationUsers> logger,
            IApplicationUsers applicationUsers,
            SignInManager<ApplicationUser> signInManager,
            IHttpContextAccessor httpContextAccessor)
        {
...
            this.signInManager = signInManager;
            this.httpContextAccessor = httpContextAccessor;
        }
...
        public async ValueTask<User?> GetSignInUserAsync()
        {
            ClaimsPrincipal? user = httpContextAccessor.HttpContext?.User;
            if (user == null)
            {
                return null;
            }
            if(signInManager.IsSignedIn(user) == false)
            {
                return null;
            }
            string? userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
            if(string.IsNullOrEmpty(userId) ||
                int.TryParse(userId, out var id) == false)
            {
                return null;
            }
            ApplicationUser? appUser = await applicationUsers.GetByIdAsync(id);
            if (appUser == null)
            {
                return null;
            }
            return new User(appUser.Id, appUser.UserName, appUser.Organization, appUser.Email);
        }
...
Enter fullscreen mode Exit fullscreen mode

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (2)

Collapse
 
fredymorales83 profile image
Edwin Fredy Morales Morales

great post, I hope implement some like this in future

Collapse
 
masanori_msl profile image
Masui Masanori

Thank you so much :)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay