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

[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] Use WebSocket with ws

Intro Until last time , I had used node-web-rtc to try WebRTC. But because the example was a little complicated for I understood the core functions of using WebRTC. So I look for other frameworks or libraries. PeerJS is a famous library for WebRTC. peers/peerjs: Peer-to-peer data in the browser. - GitHub peers/peerjs-server: Server for PeerJS - GitHub PeerJS - Simple peer-to-peer with WebRTC A problem is I don't know how to integrate to the Nest.js project. I couldn't find examples. So I don't choose at least this time. What shall I choose? According MDN, WebRTC doesn't specify strictly what technology is used on server application for connecting two devices. Signaling and video calling - Web APIs | MDN But in many examples include MDN's one use WebSocket. samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub So I try WebSocket in the Nest.js project. Use WebSocket in a Nest.js project Nest.js has a function for using We

[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