[ASP.NET Core 2 Identity] APIでユーザー登録とアクセスを管理(JWT) 2. 基本メソッド編 (ログイン)

# 前回からのあらすじ
前回：[ASP.NET Core 2 API x Identity でユーザー登録とアクセスを管理(JWT) 2. 基本メソッド編 (アカウント作成)](https://qiita.com/syantien/items/2ec6e253855202a0a3e6)

おはようございます、昨日はAPI経由でASP.NET Identityを使って新しアカウント作成してみました。今日は作ったアカウントを使い、ログインするAPIを作ります。ログインにはJWTokenを使います。ログイン情報に対してTokenを返す流れになります。
# モデルの作成

## リクエストモデル
まずはリクエスト、レスポンスモデルを準備します。 ユーザー名、パスワード、デバイスIDを受取れるようにしておきます。

```csharp:/Models/ApplicationUsers/RequestModels/ApplicationUserRequestModel.cs
using SampleApi.Models.ApplicationUsers.DataModels;

namespace SampleApi.Models.ApplicationUsers.RequestModels
{
    public class ApplicationUserRequestModel
    {
        public ApplicationUserDataModel CreateUserRequestModelToApplicationUserDateModel(
            CreateUserRequestModel user)
        {
            return new ApplicationUserDataModel()
            {
                UserName = user.UserName,
                Email = user.Email
            };
        }
    }

    public class CreateUserRequestModel
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }

    public class LoginRequestModel
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Device { get; set; }
    }
}
```

## レスポンスモデル

基本はトークンを返すだけです。エラー時の場所も宣言しておきます。

```csharp:/Models/ApplicationUsers/ResponseModels/ApplicationUserResponseModel.cs
using System;
using System.Collections.Generic;
using SampleApi.Models.ApplicationUsers.DataModels;

namespace SampleApi.Models.ApplicationUsers.ResponseModels
{
    public class ApplicationUserResponseModel
    {

        /// <summary>
        /// From ApplicationUserDataModel to CreateUserResponseModel
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public CreateUserResponseModel ApplicationUserDataModelToCreateUserRequestMode(
            ApplicationUserDataModel user)
        {
            var response = new CreateUserResponseModel()
            {
                UserName = user.UserName,
                Email = user.Email,
                Created = user.Created
            };
            
            return response;
        }
    }

    
    public class CreateUserResponseModel
    {
        public string UserName { get; set; }
      
        public string Email { get; set; }
        public DateTime Created { get; set; }

        public List<ErrorModel> Errors { get; set; }
    }

    public class LoginResponseModel
    {
        public string Token { get; set; }
        public DateTime Expires { get; set; }
        public List<ErrorModel> Errors { get; set; }
    }
}
```

## 設定データモデル
今回はJWTを利用するにあたり、いくつかの設定プロパティーをappsettings.jsonに格納します。設定した値を取得するためのモデルを用意します。

```csharp:/AppSettings.cs
namespace SampleApi
{
    public class AppSettings
    {
        public JwtConfigurableOptions JwtSetting { get; set; }
    }
    
    public class JwtConfigurableOptions
    {
        public string JwtKey { get; set; }
        public string JwtIssuer { get; set; }
        public string JwtAudience { get; set; }
        public int JwtExpireDays { get; set; }
    }
}
```

# Handlerの作成

コアとなるHandlerを作成します。ここでは2つのHandlerを作成します。
1. ログインリクエストデータを処理してログインレスポンスデータを返すもの。
2. ログインデータをトークン化するもの。

## トークン化Handler

```csharp:/Handlers/Accounts/JwtHandler.cs
using SampleApi.Handlers.Accounts.Interfaces;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace SampleApi.Handlers.Accounts
{
    public class JwtHandler : IJwtHandler
    {
        private readonly JwtConfigurableOptions _jwtConfigurableOptions;

        public JwtHandler(JwtConfigurableOptions jwtConfigurableOptions)
        {
            _jwtConfigurableOptions = jwtConfigurableOptions;
        }

        /// <summary>
        /// Take user name, device, roles and generate encoded token.
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="device"></param>
        /// <param name="roles"></param>
        /// <returns>token string</returns>
        public string GenerateEncodedToken(string userName, string device, IList<string> roles = null)
        {
            List<Claim> claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, userName),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.Ticks.ToString(), ClaimValueTypes.Integer64),
                new Claim(ClaimTypes.System, device)
            };

            if (roles?.Any() == true)
            {
                foreach (string role in roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }
            }

            // Create the JWT security token and encode it.
            JwtSecurityToken jwt = new JwtSecurityToken(
            issuer: _jwtConfigurableOptions.JwtIssuer,
            audience: _jwtConfigurableOptions.JwtAudience,
            claims: claims,
            expires: DateTime.UtcNow.AddDays(_jwtConfigurableOptions.JwtExpireDays),
            signingCredentials: new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfigurableOptions.JwtKey)),
                SecurityAlgorithms.HmacSha256)
        );

            string encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            return encodedJwt;
        }
    }
}
```

## アカウントHandler

```csharp:/Handlers/Accounts/AccountHandler.cs
using SampleApi.Handlers.Accounts.Interfaces;
using SampleApi.Models;
using SampleApi.Models.ApplicationUsers.DataModels;
using SampleApi.Models.ApplicationUsers.RequestModels;
using SampleApi.Models.ApplicationUsers.ResponseModels;
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SampleApi.Handlers.Accounts
{
    /// <summary>
    /// Handles all account related operations
    /// </summary>
    public class AccountHandler : IAccountHandler
    {

        private readonly UserManager<ApplicationUserDataModel> _userManager;
        private readonly IJwtHandler _jwtHandler;

        public AccountHandler(UserManager<ApplicationUserDataModel> userManager,
            IJwtHandler jwtHandler)
        {
            _userManager = userManager;
            _jwtHandler = jwtHandler;
        }

        /// <summary>
        /// Create new user
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public async Task<CreateUserResponseModel> Create(CreateUserRequestModel user)
        {
            // Request model
            ApplicationUserRequestModel request = new ApplicationUserRequestModel();

            // Request model to data model
            ApplicationUserDataModel applicationUser = request.CreateUserRequestModelToApplicationUserDateModel(user);

            // Timestamping
            applicationUser.Created = DateTime.UtcNow;

            // Create new user
            IdentityResult newUser = await _userManager.CreateAsync(
                applicationUser,
                user.Password);

            if (newUser.Succeeded)
            {
                // Response model
                ApplicationUserResponseModel response = new ApplicationUserResponseModel();

                // Return created user
                return response.ApplicationUserDataModelToCreateUserRequestMode(applicationUser);

            }

            // Error
            CreateUserResponseModel errorResponse = new CreateUserResponseModel();

            if (newUser.Errors != null)
            {
                errorResponse.Errors = new List<ErrorModel>();

                foreach (IdentityError error in newUser.Errors)
                {
                    errorResponse.Errors.Add(
                        new ErrorModel()
                        {
                            ErrorCode = error.Code,
                            ErrorDescription = error.Description
                        });
                }
            }

            return errorResponse;
        }


        /// <summary>
        /// Validate user id, password then generate token (as mean of login)
        /// </summary>
        /// <param name="login"></param>
        /// <returns></returns>
        public async Task<LoginResponseModel> Login(LoginRequestModel login)
        {

            LoginResponseModel response = new LoginResponseModel();

            // Check user name
            ApplicationUserDataModel user = await _userManager.FindByNameAsync(login.UserName);

            if (user != null)
            {

                // Check password
                bool isPasswordOk = await _userManager.CheckPasswordAsync(user, login.Password);

                if (isPasswordOk)
                {
                    // Get roles
                    var roles = await _userManager.GetRolesAsync(user);
                    
                    // If sucess then generate token
                    response.Token = _jwtHandler.GenerateEncodedToken(user.UserName, login.Device, roles);

                    return response;
                }

                response.Errors = new List<ErrorModel>()
                    {
                        new ErrorModel()
                        {
                            ErrorCode = "400",
                            ErrorDescription = "Invalid password"
                        }
                    };

                return response;
            }

            response.Errors = new List<ErrorModel>()
                {
                    new ErrorModel()
                    {
                        ErrorCode = "400",
                        ErrorDescription = "Invalid user"
                    }
                };

            return response;
        }
    }
}
```

# Filter

Headerでトークンを利用してセキュリティーをかけるので、ヘッダーフィルターを事前に構築します。

```csharp:AddAuthorizationHeaderParameterOperationFilter.cs
public class AddAuthorizationHeaderParameterOperationFilter : IOperationFilter
{
  public void Apply(Operation operation, OperationFilterContext context)
  {
    if (operation.Parameters == null)
    {
      operation.Parameters = new List<IParameter>();
    }

    operation.Parameters.Add(new HeaderParameter()
    {
      Name = "Device-Id",
      In = "header",
      Type = "string",
      Required = false
    });
  }
}

internal class HeaderParameter : NonBodyParameter
{
}
```

# DI

モデルとハンドラーが準備できあたら、JWTの各種設定とDIを準備します。ここではトーケンが利用できるように各種プロパティーを設定し、新しく作ったHandlerをControllerからDIできるように設定します。


```csharp:/Startup.cs
using SampleApi.Filters;
using SampleApi.Handlers;
using SampleApi.Handlers.Accounts;
using SampleApi.Handlers.Accounts.Interfaces;
using SampleApi.Models.ApplicationUsers.DataModels;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.IdentityModel.Tokens;

namespace SampleApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IOptions<AppSettings> optionsAccessor)
        {
            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)
        {

            // App Setting
            var appSettingSection = Configuration.GetSection("AppSettings");
            AppSettings appSettings = new AppSettings();
            appSettingSection.Bind(appSettings);

            // Sql
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("UserDabtaseConnection")));

            services.AddIdentity<ApplicationUserDataModel, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddUserManager<UserManager<ApplicationUserDataModel>>()
                .AddDefaultTokenProviders();

            services.Configure<IdentityOptions>(
                options =>
                {
                    // Password settings
                    options.Password.RequireDigit = false;
                    options.Password.RequiredLength = 6;
                    options.Password.RequireNonAlphanumeric = false;
                    options.Password.RequireUppercase = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequiredUniqueChars = 3;

                    // Lockout settings
                    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                    options.Lockout.MaxFailedAccessAttempts = 5;
                    options.Lockout.AllowedForNewUsers = true;

                    // User settings
                    options.User.RequireUniqueEmail = true;
                });

            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = appSettings.JwtSetting.JwtIssuer,
                ValidAudience = appSettings.JwtSetting.JwtAudience,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(
                        appSettings.JwtSetting.JwtKey)),

                RequireExpirationTime = false,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(configureOptions =>
            {
                configureOptions.ClaimsIssuer = appSettings.JwtSetting.JwtIssuer;
                configureOptions.TokenValidationParameters = tokenValidationParameters;
                configureOptions.SaveToken = true;
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddTransient<IAccountHandler, AccountHandler>();
            services.AddSingleton<IJwtHandler, JwtHandler>();

            services.AddSingleton(config => appSettings.JwtSetting);

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            // Register the Swagger generator, defining 1 or more Swagger documents
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Title = "{title}",
                    Version = "1",
                    Description = "{description}",
                    Contact = new Contact
                    {
                        Name = "{name}",
                        Email = "{email}",
                        Url = "{url}"
                    }
                });

                c.OperationFilter<AddAuthorizationHeaderParameterOperationFilter>();

                // Set the comments path for the Swagger JSON and UI.
                string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                c.IncludeXmlComments(xmlPath);
            });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 
            // specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jeton API V1");

                c.RoutePrefix = string.Empty;
            });

            app.UseAuthentication();
            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}
```

# Controllerの作成

最後にAPIのインターフェイスとなるとControllerを作成します。

```csharp:/Controllers/AccountController.cs
using System.Threading.Tasks;
using JetonApi.Handlers.Accounts.Interfaces;
using JetonApi.Models.ApplicationUsers.RequestModels;
using JetonApi.Models.ApplicationUsers.ResponseModels;
using Microsoft.AspNetCore.Mvc;

namespace JetonApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        private readonly IAccountHandler _accountHandler;

        public AccountController(IAccountHandler accountHandler)
        {
            _accountHandler = accountHandler;
        }
        
        [HttpPost, Route("/Account/CreateUser")]
        public async Task<ActionResult<CreateUserResponseModel>> Create([FromBody] CreateUserRequestModel user)
        {
            var response = await _accountHandler.Create(user);

            return response;
        }

        [HttpPost, Route("/Account/Login")]
        public async Task<ActionResult<LoginResponseModel>> Login([FromBody] LoginRequestModel login)
        {

            var response = await _accountHandler.Login(login);

            return response;
        }

    }
}
```

# Swaggerからログインのテスト

では、早速APIを試してみます。

## 正常リクエスト
（パスワードのスペルを前回間違えいました・・）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/241909/d3e983a2-c36f-2eb9-bfe6-27d2169c011b.png)

## 正常レスポンス
それっぽいのが返ってきていますね。（値の確認は次にやります）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/241909/d5142343-1040-383a-d1a9-68798e167b26.png)

## 異常リクエスト/レスポンス 存在しないユーザー
存在しないユーザーでリクエスト
![image.png](https://qiita-image-store.s3.amazonaws.com/0/241909/ae8ce8c7-ce4e-815c-f3c2-46ed0307335c.png)

## 異常リクエスト/レスポンス パスワード間違え
パスワード間違えてリクエスト
![image.png](https://qiita-image-store.s3.amazonaws.com/0/241909/b430961e-0d22-8c0d-7047-7bba983ac523.png)

# 次

今回は正しいユーザー名とパスワードに対してトークンを発行しました。この発行したトークンを解読してアプリケーションを構築していきます。