タスク管理ツールをReact + ASP.Coreで作る018トークン機能追加


[前回](https://qiita.com/mosuma/items/e606cd33423edfdb4118)の続きです。
#016より数回に分けて認証機能を導入しています




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

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

# 概要

前回までで、ログイン機能は備わりましたが、このままだとログイン状態に合わせて操作を制御するということがまだできません。

サーバー側ではトークンを生成してクライアントに返す機能を、クライアントではトークンを取得し、まずは表示する機能を追加します。

(最終的には取得したトークンをサーバー問い合わせの際に付けて送るようにし、かつサーバー側では認証が必要な処理は適切なトークンが来た場合にのみ許可する仕様にすることで、認証機能の一通りは完成となります。その段階までは、後の記事で紹介します)


トークン機能を追加します。

作りこむと複雑になってしまうので、まずはサーバー側にトークンを生成してログイン成功時には返す機能を、クライアント側ではトークンを取得したら表示する機能を付けていきます。




# 構築

## 本記事で実施する内容のサマリー

- サーバーサイド
    - jwtトークン生成用の機能を追加
    - ログイン時にjwtトークンを返す処理を追加
- クライアントサイド
    - ログイン時にjwtトークンを受け取るようにコードを変更




## サーバーサイドのコード追加


### JwtService.cs


Jwtトークンを生成する機能を新たに追加します

```diff_c_sharp:JwtService.cs
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using server_app.Models;

namespace server_app.Services
{
    public class TokenService
    {
        private readonly IConfiguration _config;
        public TokenService(IConfiguration config)
        {
            _config = config;
        }

        public string CreateToken(ApplicationUser user)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Email, user.Email),
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["TokenKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.Now.AddDays(7),
                SigningCredentials = creds
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            var token = tokenHandler.CreateToken(tokenDescriptor);

            return tokenHandler.WriteToken(token);            

        }
    }
}
```

### 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.Services;

namespace server_app.Controllers
{
    [AllowAnonymous]
    [ApiController]
    [Route("[controller]")]
    public class AccountController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly  SignInManager<ApplicationUser> _signinManager;
+        private readonly TokenService _tokenService;
        public AccountController(UserManager<ApplicationUser> userManager
            ,SignInManager<ApplicationUser> signinManager
+            ,TokenService tokenService
             )
        {
+            _tokenService = tokenService;
            _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
            {
+                    Token = _tokenService.CreateToken(user),
                    Username = user.UserName
            };
        }
    }
}
```

### UserModel.cs

```diff_c_sharp:UserModel.cs

namespace server_app.Models
{
    public class UserModel
    {
+        public string Token { get; set; }
        public string Username {get; set; }
        
    }
}
```

### Startup.cs

トークン生成機能の追加と、Cros関連の変更を行います

```diff_c_sharp:Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
+ using System.Text;
using System.Threading.Tasks;
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
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.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using server_app.Models.EDM;
+ using server_app.Services;

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>();
            
+           //for jwt token
+           var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["TokenKey"]));
+
+           services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+               .AddJwtBearer(opt =>
+               {
+                   opt.TokenValidationParameters = new TokenValidationParameters
+                   {
+                       ValidateIssuerSigningKey = true,
+                       IssuerSigningKey = key,
+                       ValidateIssuer = false,
+                       ValidateAudience = false
+                   };
+               });
+           services.AddScoped<TokenService>();
+           //--------------------------------------------------------------------------------------

           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.UseAuthentication();
            
            app.UseAuthorization();

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

```

### appsettings.json
トークン生成用のキーを追加します
```diff_json:appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection":"Data Source=database.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
+  "TokenKey":"key for jwt token"
}

```


## クライアントサイドのコード追加

### Login.tsx

ログイン成功時に帰ってくるトークンを表示できるように変更します

```diff_tsx:Login.tsx
import React, { SyntheticEvent, useState } from 'react';

const Login = (
    ) => 
{
    
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [resultcode, setResultcode] = useState(0);
    const [resultTitle, setResultTitle] = useState('');
+    const [token, setToken] = useState('');



    const submit = async (e: SyntheticEvent) => {
        e.preventDefault();
        const response = await fetch('https://localhost:5001/account/login',
        {
            method : 'POST',
            headers:{'Content-Type' : 'application/json'},
            body: JSON.stringify({
                email,
                password
            })
        });
        const content = await response.json();
        const status = await response.status

        setResultcode(status);
        setResultTitle(content.title);
        if(status==200){
            setName(content.username);
+            setToken(content.token);

        }

    }

    
    return (
        <>
        <form onSubmit={submit}>
            <h2>Sign in</h2>

            <ul>

                <li>
                    <label>email</label>
                    <input type="email" placeholder="name@example.com" required 
                        onChange = {e => setEmail(e.target.value)}            
                    />
                </li>

                <li>                    
                    <label>password</label>
                    <input type="password" placeholder="Password" required 
                        onChange = {e => setPassword(e.target.value)}            
                    />
                </li>
            </ul>

            <button type="submit">Sign in</button>

        </form>
        
        <h2>Response</h2>

        <ul>
            <li>
                {resultcode!=0 && <>{resultcode==200 ? <>Login Success</> : <>Login Fail</>}</>}
            </li>

            <li>
                {resultcode==200 && <>Name:{name}</>}
            </li>

            <li>
                {resultcode!=0 && <>Code:{resultcode}</>}
            </li>

            <li>
                {resultcode!=0 && <>msg:{resultTitle}</>}
            </li>
+
+            <li>
+                {resultcode!=0 && <p>token : {token}</p>}
+            </li>
        </ul>
        </>
    );

}

export default Login;
```



# 実行結果

実行すると以下の様に取得されたトークンが表示されるようになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2664470/0347f873-8abe-e121-f3ad-154b7da8acf0.png)




今回は以上です。
続きは[次回](https://qiita.com/mosuma/items/82ae859577ab676c06bc)です