【.NET 5】【ASP.NET Core Identity】SignIn with custom user
Masui Masanori
Posted on February 3, 2021
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
- 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 "";
}
}
}
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>();
}
}
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();
});
}
}
}
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'.
...
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;
}
}
}
}
Startup.cs
...
namespace ApprovementWorkflowSample
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
// OK
services.AddIdentity<ApplicationUser, IdentityRole<int>>()
.AddUserStore<ApplicationUserStore>()
.AddEntityFrameworkStores<ApprovementWorkflowContext>()
.AddDefaultTokenProviders();
...
Resources
- Introduction to Identity on ASP.NET Core | Microsoft Docs
- ASP.NET Identityカスタマイズに挑戦 - かずきのBlog@hatena
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();
}
}
}
"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);
}
...
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);
}
...
Posted on February 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.