ASP.NET(.NET5)のAPIでJWT利用


.net環境でJWTつかう機会があるのでメモ。[node番はこちら](https://qiita.com/zaburo/items/985db4ccbb0cdb5c9095)。

## 主な手順

利用の流れは下記のような感じ。

* Startup.csにてJWTの認証およびチェック基準を設定、有効化
* Token発行ようのControllerを実装（簡易認証OKならToken発行）
* 認証を付けたいAPIに[Authorize]を追加し動作チェック


## 前提

私は基本Mac + CLIです。

* macでdotnet cli利用
* ver 5.x（たぶん3.1でも動く）

## 準備

### 作業場の確保

では、作業場の確保と必要ライブラリのインストール。

```bash
dotnet new api -o dotnet-jwt
cd dotnet-jwt

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

### リダイレクトの無効化

Startup.csのConfigure()中のapp.UseHttpsRedirection();をコメントアウトしておきましょう。
強制的にhttpsにリダイレクトされるのを無効化できます。

```
// app.UseHttpsRedirection();
```

### 動作確認

今回は最初から参考実装されているWeatherforecastControllerのGet()メソッドに認証を追加するので、その動作を確認しておきます。
起動して、

```bash
dotnet run
```

リクエストするとJSONが戻ってきます。

```bash
curl -X GET http://localhost:5000/weatherforecast

{"date":"2021-01-31T09:37:33.3741+09:00","temperatureC":44,"temperatureF":111,"summary":"Warm"},{"date":"2021-02-01T09:37:33.374102+09:00","temperatureC":28,"temperatureF":82,"summary":"Bracing"},{"date":"2021-02-02T09:37:33.374102+09:00","temperatureC":-8,"temperatureF":18,"summary":"Balmy"},{"date":"2021-02-03T09:37:33.374102+09:00","temperatureC":39,"temperatureF":102,"summary":"Warm"}
```

## 実装

では、まずJWT認証を設定し、有効化します。いろいろ記述していますが、大きな流れとしては下記の通り。

* ConfigureServices()にてservices.AddAuthentication()を追加（設定）
* Configure()にてapp.UseAuthentication()を追加（有効化）

### Startup.cs

では、上記を意識しながら実装します。

```csharp: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.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;

//追加
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace dotnet_jwt
{
    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.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
                {
                    options.TokenValidationParameters =
                    new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = "hoge",
                        ValidAudience = "hoge",
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("12345678901234567890"))
                    };
                });

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

        // 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", "dotnet_jwt v1"));
            }

            // app.UseHttpsRedirection();

            app.UseRouting();

            //追加
            app.UseAuthentication();
            
            app.UseAuthorization();

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

### TokenController.cs

次にTokenを発行するControllerを実装します。WeatherforecastControllerに実装してもいいのですが、ネットで多くの方が参考にしているAuth0のサンプルではこのような構造になっているようです。
いちおう、簡単な認証（静的なID,PWの一致）を実装していますが、ここを目的に応じた認証にかえるといいでしょう。

```csharp:TokenController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

//追加
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;

namespace dotnet_jwt.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class TokenController : ControllerBase
    {
        [AllowAnonymous]
        [HttpPost]
        public IActionResult Login([FromBody] LoginModel login)
        {
            //POST受け取り
            string username = login.Username;
            string password = login.Password;
            
            //ダミー認証
            if (username == "hoge" && password == "password")
            {   
                //token生成（下の関数で）
                var tokenString = BuildToken();
                var response = new
                {
                    token = tokenString
                };

                return Ok(response);
            }
            else
            {
                return BadRequest();
            }

        }

        //token生成関数
        private string BuildToken()
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("12345678901234567890"));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: "hoge",
                audience: "hoge",
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds
            );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    //loginモデル
    public class LoginModel
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }


}
```

### Weatherforecast.cs（抜粋）

実装が完了したら認証を適用します。以下は抜粋です。

using Microsoft.AspNetCore.Authorization;を追加し、Get()に[Authorize]を追加。

```csharp:weatherforecast.cs
        [Authorize]
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
```

##　動作確認

まず、普通にリクエストしてみます。先述のものに-vオプションを追加してやりとりを表示してみます。
すると、認証エラーになっているのがわかります。

### 普通にリクエスト

```diff
curl -X GET http://localhost:5000/weatherforecast -v


Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1:5000...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /weatherforecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
-< HTTP/1.1 401 Unauthorized
< Date: Fri, 29 Jan 2021 00:45:50 GMT
< Server: Kestrel
< Content-Length: 0
< WWW-Authenticate: Bearer
< 
* Connection #0 to host localhost left intact
```

### tokenの発行

では、認証用のtokenを取得してみます。
適切なusernameとpasswordを付与してPOSTするとtokenが得られます。

```bash
curl -X POST  http://localhost:5000/api/token  -H "Content-Type: application/json" -d '{"username":"hoge","password":"password"}'

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE4ODMwNzgsImlzcyI6ImhvZ2UiLCJhdWQiOiJob2dlIn0.GytQKuz_lyeDU4RjkVEBpUFQ95JirYfA1PeYurquRn8"}
```

### tokenを付与してリクエスト

tokenをヘッダーに付与してリクエストするとJSONが得られました。

```bash
curl -v -X GET  http://localhost:5000/weatherforecast -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE4ODMwNzgsImlzcyI6ImhvZ2UiLCJhdWQiOiJob2dlIn0.GytQKuz_lyeDU4RjkVEBpUFQ95JirYfA1PeYurquRn8"


{"date":"2021-01-31T09:49:46.110224+09:00","temperatureC":30,"temperatureF":85,"summary":"Cool"},{"date":"2021-02-01T09:49:46.110225+09:00","temperatureC":2,"temperatureF":35,"summary":"Bracing"},{"date":"2021-02-02T09:49:46.110226+09:00","temperatureC":19,"temperatureF":66,"summary":"Bracing"},{"date":"2021-02-03T09:49:46.110226+09:00","temperatureC":28,"temperatureF":82,"summary":"Sweltering"}]
```

以上です。