タスク管理ツールをReact + ASP.Coreで作る 016サーバーサイドに認証機能を追加する

[前回](https://qiita.com/mosuma/items/75d387607cebdfc22bee)の続きです

今回から数回に分けて認証機能を導入します

### ガイド
全体Index：[タスク管理ツールをReact + ASP.Coreで作る - Index](https://qiita.com/mosuma/items/88e40dc44e7bff1a54d8)

## 認証機能追加の一連の流れ
- 016サーバーサイドに認証機能関連のクラス追加(本記事)
- [017クライアントにユーザー登録・ログイン画面と処理を追加](https://qiita.com/mosuma/items/e606cd33423edfdb4118)
- [018認証トークン関連機能をサーバー・クライアント双方に追加](https://qiita.com/mosuma/items/035967eb98259bbd4934)
- [019認証有無に従い制御を実施する機能を追加](https://qiita.com/mosuma/items/82ae859577ab676c06bc)
- [020認証機能をリファクタリング](https://qiita.com/mosuma/items/cf50dc4136fe06233412)
- [021認証状態に従い画面表示を切り替え](https://qiita.com/mosuma/items/5b7ed9431633800e7cc6)


# 構築

## 本記事で実施する内容のサマリー
- パッケージをインストール
- 認証機能にかかわるコードを追加
    - ユーザーを管理のためのテーブルの原資になるクラスを追加
    - DBに認証ユーザーを管理するテーブルを追加する様に、コンテキストを変更
    - 認証関連操作(ログイン・新規登録・認証情報からログイン中のユーザー情報取得)のためのデータモデルを追加
    - 認証関連操作の為のコントローラークラスの追加
    - webサービス機能の中に認証機能を追加
- マイグレーションしてデータベースを更新


## パッケージをインストール

認証機能を有効にするため、以下のパッケージをNugetギャラリーからインストールします

- 認証機能関連
    - Microsoft.AspNetCore.Authentication.JwtBearer
    - Microsoft.AspNetCore.Identity.UI




## コード追加・変更

準備ができたら認証関連のコードを追加していきます。
以下のファイルを追加or変更します


| ファイル名 | 変更内容 | 概要 |
|  ---  | ---   |---|
| ApplicationUser.cs | 新規追加 |ユーザー管理テーブル用モデルクラスを新規追加 |
| DataContext.cs | 変更 | コンテキストクラスを認証機能付きに変更     |
| AccountController.cs | 新規追加 |ログイン・ユーザー登録処理・結果取得を行うコントローラー |
| LoginModel.cs | 新規追加 | ログイン処理時にコントローラーにデータを渡すためのモデルクラス     |
| RegisterModel.cs | 新規追加 |ユーザー登録時にコントローラーにデータを渡すためのモデルクラス |
| UserModel.cs | 新規追加 |ログイン・ユーザー登録後にコントローラーから結果を返す際に使用するクラス |
| Startup.cs | 変更 | 認証関連の処理を追加する     |








### ApplicationUser.cs

```diff_c_sharp:ApplicationUser.cs
using Microsoft.AspNetCore.Identity;

namespace server_app.Models.EDM
{
    public class ApplicationUser : IdentityUser
    {
    }
}
```

### DataContext.cs

```diff_c_sharp:DataContext.cs

+ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace server_app.Models.EDM
{
+   public class DataContext : IdentityDbContext<ApplicationUser>
    {
+       public DataContext(DbContextOptions<DataContext> options) : base(options)
        {
        }

        public DbSet<t_task> t_tasks {get; set;}
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
+           base.OnModelCreating(modelBuilder);
            
            modelBuilder.Entity<t_task>(entity =>
            {
                entity.HasKey(e => new { e.id_task });

            });
        }
    }
}
```

### AccountController.cs

```diff_c_sharp:AccountController.cs
using System.Security.Claims;
using System.Threading.Tasks;
using server_app.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using server_app.Models.EDM;
using server_app.Models.DTO;

namespace server_app.Controllers
{
    [AllowAnonymous]
    [ApiController]
    [Route("[controller]")]
    public class AccountController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly  SignInManager<ApplicationUser> _signinManager;
        public AccountController(UserManager<ApplicationUser> userManager
            ,SignInManager<ApplicationUser> signinManager
             )
        {
            _signinManager = signinManager;
            _userManager = userManager;
        }

        [HttpPost("login")]
        public async Task<ActionResult<UserModel>> Login(LoginModel loginModel)
        {
            
            var user = await _userManager.FindByEmailAsync(loginModel.Email);

            if(user == null) return Unauthorized();

            var result = await _signinManager.CheckPasswordSignInAsync(user, loginModel.Password, false);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return Unauthorized();
        }

        [HttpPost("register")]
        public async Task<ActionResult<UserModel>> Register(RegisterModel registerModel)
        {
            if(await _userManager.Users.AnyAsync(x => x.Email == registerModel.Email))
            {
                ModelState.AddModelError("email", "Email taken");
                return ValidationProblem();
            }
            if(await _userManager.Users.AnyAsync(x => x.UserName == registerModel.Username))
            {
                ModelState.AddModelError("username", "Username taken");
                return ValidationProblem();
            }

            var user = new ApplicationUser
            {
                Email = registerModel.Email,
                UserName = registerModel.Username
            };

            var result = await _userManager.CreateAsync(user, registerModel.Password);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return BadRequest("Problem regist User");
        }
        private UserModel CreateUserObject(ApplicationUser user)
        {
            return new UserModel
            {
                    Username = user.UserName
            };
        }
    }
}
```

### LoginModel.cs

```diff_c_sharp:LoginModel.cs

namespace server_app.Models.DTO
{
    public class LoginModel
    {
        public string Email {get; set;}
        public string Password {get; set;}
        
    }
}
```

### RegisterModel.cs

```diff_c_sharp:RegisterModel.cs


using System.ComponentModel.DataAnnotations;

namespace server_app.Models.DTO
{
    public class RegisterModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set;}
        [Required]
        [RegularExpression("(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$", ErrorMessage ="Password must be complex")]
        public string Password {get; set; }
        [Required]
        public string Username { get; set; }

        
    }
}
```


### UserModel.cs

```diff_c_sharp:UserModel.cs
namespace server_app.Models.DTO
{
    public class UserModel
    {
        public string Username {get; set; }
        
    }
}
```



### Startup.cs

```diff_c_sharp:Startup.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
+ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using server_app.Models.EDM;

namespace server_app
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DataContext>(opt =>
            {
             opt.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
            } );

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "server_app", Version = "v1" });
            });

+             services.AddDefaultIdentity<ApplicationUser>(
+                 options => {
+                                options.SignIn.RequireConfirmedAccount = false;
+                             }
+                 )
+                 .AddEntityFrameworkStores<DataContext>();
            
           services.AddCors(o => o.AddPolicy(MyAllowSpecificOrigins, builder =>
           {
               builder.AllowAnyOrigin()    // Allow CORS Recest from all Origin
                      .AllowAnyMethod()    // Allow All Http method
                      .AllowAnyHeader();   // Allow All request header
           }));
        }

        readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "server_app v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCors(MyAllowSpecificOrigins);   // Add For CORS

            app.UseAuthorization();

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


```

## データベース更新

コードの追加が出来たので、データベースを生成します。
以下のコマンドを実行します

```
dotnet ef migrations add addIdentity
dotnet ef database update
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2664470/705fbac5-a9a3-c919-61aa-a2b6f27f381f.png)

## 動作

ここまでの流れで認証機能の基本部分を導入しました。
各APIに認証状態に沿ってアクセス制限を行う設定を入れていないので、この時点では動作させてもこれまでと動きに違いはないので、動作は省略します。

続きは[次回](https://qiita.com/mosuma/items/e606cd33423edfdb4118)です


TAG:V000-13-00