スキップしてメイン コンテンツに移動

[ASP.NET Core 3.0] Sign in with Identity

Today I tried authentication by ASP.NET Core Identity.

What I did

1. Create custom IdentityUser
2. Create a route for authentication
3. After authentication, access authorized route

Setup

Most of all environments were same as previous post.

I only installed Microsoft.AspNetCore.Identity.EntityFrameworkCore (ver.3.0.0) by NuGet.

Add user table

I added user table to DB.


CREATE TABLE "User"
(
	"UserId" serial PRIMARY KEY,
	"Name" text not null,
	"Password" text not null
)


1. Create custom IdentityUser

First, I created custom IdentityUser for signing in.
Because the default IdentityUser had had so many properties.

But I only wanted three things below.
  1. Id
  2. UserName
  3. Password

ApplicationUser

I could created custom user class by inheritting IdentityUser class.

ApplicationUser.cs


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

namespace Models
{
    [Table("User")]
    public class ApplicationUser: IdentityUser
    {
        // set null on creating user because the column data type is serial.
        [Key]
        [Column("UserId")]
        public int? ApplicationUserId { get; set; }
        [Column("Name")]
        public override string UserName { get; set; }
        [Column("Password")]
        public override string PasswordHash { get; set; }
        
        // I didn't want to use them.
        [NotMapped] public override string Id { get; set; }
        [NotMapped] public override string NormalizedUserName { get; set; }
        [NotMapped] public override string Email { get; set; }
        [NotMapped] public override string NormalizedEmail { get; set; }
        [NotMapped] public override bool EmailConfirmed { get; set; }        
        [NotMapped] public override string SecurityStamp { get; set; }
        [NotMapped] public override string ConcurrencyStamp { get; set; }
        [NotMapped] public override string PhoneNumber { get; set; }
        [NotMapped] public override bool PhoneNumberConfirmed { get; set; }
        [NotMapped] public override bool TwoFactorEnabled { get; set; }
        [NotMapped] public override DateTimeOffset? LockoutEnd { get; set; }
        [NotMapped] public override bool LockoutEnabled { get; set; }
        [NotMapped] public override int AccessFailedCount { get; set; }

        public void SetValue(string userName, string password)
        {
            UserName = userName;
            // set hashed password text to PasswordHash.
            PasswordHash = new PasswordHasher<ApplicationUser>()
                .HashPassword(this, password);   
        }
    }
}


The reason why I put [NotMapped] to some properties was there had been only three columns in the User table.

ApplicationUserStore

For using custom IdentityUser, I had to create custom ApplicationUserStore class.

ApplicationUserStore.cs


using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Models;

namespace UpgradeSample.Models
{
    public class ApplicationUserStore: IUserPasswordStore<ApplicationUser>
    {
        private readonly UpgradeSampleContext _context;
        public ApplicationUserStore(UpgradeSampleContext context)
        {
            _context = context;
        }
        
        public void Dispose() { /* Do nothing now */ }

        public Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.ApplicationUserId.ToString(), cancellationToken);
        }
        public Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.UserName,cancellationToken);
        }
        public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        {
            await using var transaction = _context.Database.BeginTransaction();
            try
            {
                user.UserName = userName;
                _context.Users.Update(user);
                _context.SaveChanges();
                transaction.Commit();
            }
            catch (Exception e)
            {
                transaction.Rollback();
            }
        }
        public Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.UserName.ToUpper(),cancellationToken);
        }
        public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
        {
           // Do nothing
           return Task.Run(() => { }, cancellationToken);
        }
        public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            await using var transaction = _context.Database.BeginTransaction();
            try
            {
                _context.Users.Add(user);
                _context.SaveChanges();
                transaction.Commit();

                return IdentityResult.Success;
            }
            catch (Exception e)
            {
                transaction.Rollback();
                return IdentityResult.Failed();
            }
        }
        public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            await using var transaction = _context.Database.BeginTransaction();
            try
            {
                _context.Users.Update(user);
                _context.SaveChanges();
                transaction.Commit();
                return IdentityResult.Success;
            }
            catch (Exception e)
            {
                transaction.Rollback();
                return IdentityResult.Failed();
            }
        }
        public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            await using var transaction = _context.Database.BeginTransaction();
            try
            {
                _context.Users.Remove(user);
                _context.SaveChanges();
                transaction.Commit();
                return IdentityResult.Success;
            }
            catch (Exception e)
            {
                transaction.Rollback();
                return IdentityResult.Failed();
            }
        }
        public Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            return _context.Users.FirstOrDefaultAsync(u => 
                u.ApplicationUserId.ToString() == userId, cancellationToken);
        }
        public Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            return _context.Users.FirstOrDefaultAsync(u => 
                u.UserName.ToUpper() == normalizedUserName, cancellationToken);
        }
        public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
        {
            // Do nothing
            return Task.Run(() => { }, cancellationToken);
        }
        public Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.PasswordHash, cancellationToken);
        }
        public Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => 
                string.IsNullOrEmpty(user?.PasswordHash), cancellationToken);
        }
    }
}


ApplicationUserStore class inherited IUserPasswordStore<ApplicationUser>.
I hadn't known what should I do on SetPasswordHashAsync().

I should set arcument's passwordHash to user.PasswordHash and update?
So I should set hashed password as argument?

If I will understand that, I will update this post.

await using

"await using" had added from C# 8.0.

ApplicationUserStore.cs


        public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        {
            await using var transaction = _context.Database.BeginTransaction();
            try
            {
                ~ omitted ~
            }
            catch (Exception e)
            {
                transaction.Rollback();
            }
        }


Because BeginTransaction()'s return value IDbContextTransaction had inherited IAsyncDisposable, so I should use it.
Ok, I could understand.
But why I shouldn't use curly braces like below?

ApplicationUserStore.cs


        public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        {
            await using (var transaction = _context.Database.BeginTransaction())
            {
                try
                {
                ...
                }
                catch (Exception e)
                {
                    transaction.Rollback();
                }
            }
        }


JetBrains Rider suggested removing them.
But I couldn't understand.
So after I understand the reason, I will add it here.

Add codes for authentication to Startup and DB Context class

UpgradeSampleContext.cs


using Microsoft.EntityFrameworkCore;
using Models;

namespace UpgradeSample.Models
{
    public class UpgradeSampleContext: DbContext
    {
        public UpgradeSampleContext(DbContextOptions options)
            :base(options)
        {
        }
        public DbSet<ApplicationUser> Users { get; set; }
        public DbSet<Product> Products { get; set; }
    }
}


Startup.cs


using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Models;
using UpgradeSample.Models;
using UpgradeSample.Products;
using UpgradeSample.Users;

namespace UpgradeSample
{
    public class Startup
    {
        private IConfigurationRoot Configuration { get; }
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            // DB Connect
            services.AddDbContext<UpgradeSampleContext>(options =>
                options.UseNpgsql(Configuration["DbConnect"]));
            
            // Identity
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddUserStore<ApplicationUserStore>()
                .AddEntityFrameworkStores<UpgradeSampleContext>() 
                .AddDefaultTokenProviders();
            
            ...
            // Accessing user class
            services.AddScoped<IUserService, UserService>();
        }
        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {
            ...
            app.UseStaticFiles();            
            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Introduction to Identity on ASP.NET Core - Microsoft Docs

UseAuthorization

From ASP.NET Core 3.0, UseAuthorization() had been separated from UseAuthentication().
Migrate from ASP.NET Core 2.2 to 3.0 - Microsoft Docs

Because I wanted to use [Authorize] on controller class, I added it.

I had to call UseAuthentication() first.
If I called UseAuthorization() first, the exception had been occurred.
Because I had thought UseAuthorization() needed authentication info.

2. Create a route for authentication

For accessing User class

IUserService.cs


using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Models;

namespace UpgradeSample.Users
{
    public interface IUserService
    {
        Task<DisplayUser> FindByUserNameAsync(string userName);
        Task<IdentityResult> CreateUserAsync(string userName, string password);
        Task<IdentityResult> UpdateUserAsync(ApplicationUser user);
        Task<IdentityResult> DeleteUserAsync(int userId);
    }
}


DisplayUser class was just for exclude the password.

DisplayUser.cs


using Models;

namespace UpgradeSample.Users
{
    public class DisplayUser
    {
        public int UserId { get; set; } = -1;
        public string UserName { get; set; }

        public void SetUser(ApplicationUser user)
        {
            if (user?.ApplicationUserId == null)
            {
                return;
            }
            UserId = (int) user.ApplicationUserId;
            UserName = user.UserName;
        }
    }
}


UserService.cs


using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Models;

namespace UpgradeSample.Users
{
    public class UserService: IUserService
    {
        private readonly UserManager<ApplicationUser> _userManager;
        public UserService(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }
        public async Task<DisplayUser> FindByUserNameAsync(string userName)
        {
            var applicationUser = await _userManager.FindByNameAsync(userName);
            if (applicationUser == null)
            {
                return new DisplayUser();
            }
            var user = new DisplayUser();
            user.SetUser(applicationUser);
            return user;
        }
        public async Task<IdentityResult> CreateUserAsync(string userName, string password)
        {
            var existUser = await FindByUserNameAsync(userName);
            if (existUser.UserId > 0)
            {
                return IdentityResult.Failed();
            }
            var newUser = new ApplicationUser();
            newUser.SetValue(userName, password);
            return await _userManager.CreateAsync(newUser);
        }
        public async Task<IdentityResult> UpdateUserAsync(ApplicationUser user)
        {
            var targetUser = await _userManager.FindByIdAsync(user.ApplicationUserId.ToString());
            if (targetUser == null)
            {
                return IdentityResult.Failed();
            }
            targetUser.SetValue(user);
            return await _userManager.UpdateAsync(targetUser);
        }
        public async Task<IdentityResult> DeleteUserAsync(int userId)
        {
            var targetUser = await _userManager.FindByIdAsync(userId.ToString());
            if (targetUser == null)
            {
                return IdentityResult.Failed();
            }
            return await _userManager.DeleteAsync(targetUser);
        }
    }
}


Routing class

ApiController.cs


using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Models;
using UpgradeSample.Models;
using UpgradeSample.Products;
using UpgradeSample.Users;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;

namespace UpgradeSample.Controllers
{
    public class ApiController: Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly IProductService _productService;
        private readonly IUserService _userService;
        public ApiController(SignInManager<ApplicationUser> signInManager,
            IProductService productService,
            IUserService userService)
        {
            _signInManager = signInManager;
            _productService = productService;
            _userService = userService;
        }
        ...
        [Route("/sample")]
        public async Task<IdentityResult> CreateSampleUser()
        {
            return await _userService.CreateUserAsync("hello", "world");   
        }
        [Route("/sample-signin")]
        public async Task<bool> SignInBySampleUser()
        {
            SignInResult result = await _signInManager.PasswordSignInAsync("hello", "world",
                false, false);
            return result.Succeeded;
        }
        [Authorize]
        [Route("/after-sample-signin")]
        public string ShowAuthorizedPage()
        {
            // after I success signing in, show "OK".
            return "OK";
        }
    }
}


Perhaps I should move SignInManager to service class.
Now I could signin by ASP.NET Core 3.0. So I will sign in from the page of Angular.

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests) ...

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu...

[Angular] Sending file with Observable and showing loading screen

Intro When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable". [Angular][ASP.NET Core] Upload chunked files So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>. Call observables in order I sended file in these steps. Read file by FileReader Create directory for saving chunks send and saving chunks merge chunks to one file and delete chunks Each steps used the former steps result. So I could write by Promise.then like below. this.executeStep1() // return Promise<UploadResult> .then(result => this.executeStep2(result)) // return Promise<UploadResult> .then(result => this.executeStep3(result)) // return Promise<UploadResult> .catch(reason => console.log(reason)); Result I could write with pipe & flatMap. file-uploader.service.ts public upload(file: File): Observable<U...