ASP .Net Core3.1 証明書認証とJWTトークンを使う

#はじめに
「.Net Core 3.1について調べてみよう」その3です。 
今回は、ASP .Net Core 3.1 Web APIの認証方法について調べてみました。仕様を以下のように決めました。

#仕様
接続にはHTTPSを使用しWeb APIにアクセスできる端末をクライアント証明書で認証します。次に、接続するユーザは、JWT認証を行います。また、ユーザ情報は、通常、DBやLDAP（Active Directory)等から取得しますが、単純化するためプログラム内に埋め込みます。

#前提条件
今回、認証機能を追加するアプリケーションは、Visual Studio 2019の新規プロジェクト作成で[APS.NET Core Web アプリケーション]テンプレート、[API]で作成したものを使用します。ウィザードで作成したコードをビルドして、[IIS Express]ではなく、[*プロジェクト名*]で実行します。ブラウザが起動し、httpsで接続されブラウザ上にJSONが表示されることを確認しておきます。これで準備完了です。
また、サーバ証明書は、ASP.NET Core HTTPS 開発証明書（Visual Studio インストール時に自動でローカルのWindowsストアにインストールされるもの）、クライアント証明書は、自分で作成した証明局が発行する証明書を使用します。

#JWT(JSON Web Token)について


詳細について知りたい場合、以下の記事がわかりやすいです。
[認証におけるJWTの利用について](https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6)

#クライアント証明書での認証
まず、クライアント証明書での認証機能を追加します。ソースコードは、必要ない部分は一部省略しています。

```C#:Startup.cs
using Microsoft.AspNetCore.Authentication.Certificate;
using System.Security.Cryptography.X509Certificates;

public void ConfigureServices(IServiceCollection services)
{
    //証明書による認証をデフォルトとして追加します。
    services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options => 
        {
            //今回は、自分で作成した証明書を使用しているため、証明書失効リストは確認しません
            options.RevocationMode =X509RevocationMode.NoCheck;
            
            options.Events = new CertificateAuthenticationEvents 
            { 
                OnCertificateValidated = context => 
                { 
                    //TODO:証明書を確認
                    context.Success(); 
                    return Task.CompletedTask; 
                } 
            }; 
          });
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();
    app.UseRouting();
    //ConfigureServicesで追加した認証を有効化します。
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

クライアントにクライアント証明書を要求します。appsettigns.josnで設定を行いたかったのですが、現時点では対応していません。

```C#:Program.cs
using Microsoft.AspNetCore.Server.Kestrel.Https;

public static IHostBuilder CreateHostBuilder(string[] args) =>
   Host.CreateDefaultBuilder(args)
       .ConfigureWebHostDefaults(webBuilder =>
       {
           webBuilder.UseStartup<Startup>();
           //クライアントにクライアント証明書を要求します。
           webBuilder.ConfigureKestrel(o => { o.ConfigureHttpsDefaults(o => o.ClientCertificateMode = ClientCertificateMode.RequireCertificate); });
       });

```

上記の内容は、以下のURLの記事を参考にしました。
[ASP.NET Core で証明書認証を構成する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/certauth?view=aspnetcore-3.1)

#JWT認証を追加
次に、JWT認証を追加します。
JWTトークンを取得するためのlogin APIです。このAPIにアクセスする時だけ、クライアント証明書認証を行います。


```C#:WeatherForcastController.cs
        //ログインのみクライアント証明書認証を行います
        [Authorize(AuthenticationSchemes = CertificateAuthenticationDefaults.AuthenticationScheme)]
        [HttpPost]
        public IActionResult Login([FromBody]UserCredential userCredential)
        {
            //このサンプルでは、ユーザの情報を固定で判定しています
            var user = new User
            {
                Name = "test",
                Password = "test",
                Rol = "Admin"
            };
            if (userCredential.Name != user.Name || userCredential.Password != user.Password)
            {
                return Forbid();
            }

            var token = CreateToken(user);

            //JWTトークンをJSONで返します
            return Ok(new { token = token });
        }
```

upsettings.jsonに指定された内容に従って、JWTトークンを作成します。

```C#:WeatherForcastController.cs
        protected string CreateToken(User user)
        {
            var token = new JwtSecurityToken(
                issuer: configuration_["Jwt:Issuer"],
                audience: configuration_["Jwt:Audience"],
                claims: new[]
                {
                    new Claim(JwtRegisteredClaimNames.Sub, user.Name),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(ClaimTypes.Name, user.Name),
                    new Claim(ClaimTypes.Role, user.Rol)
                },
                expires: DateTime.UtcNow.AddDays(1),   //このサンプルでは、トークンンの有効期限を1日とします
                notBefore: DateTime.UtcNow,
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration_["Jwt:Key"])),
                    SecurityAlgorithms.HmacSha256)
                );

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

APIアクセス時にJWT認証を追加するため[Authorize]アノテーションを付けます。

```C#:WeatherForecastController.cs

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{

```

クライアントは、最初にこのAPIにアクセスしてJWTトークンを取得します。
以降は、クライアントはHTTPヘッダ(Authorization)にこのトークンを付加してアクセスします。
Authorization: Bearer \<token\>

#最後に

簡単ですが、ASP .Net Core 3.1 Web APIでクライアント証明書とJWT認証を組み合わせる方法を検証しました。クライアント証明書を使用する方法は、あまり使われないためか紹介されている記事が少ないと思います。2要素認証にショートメールやワンタイムパスワードを使う方法は実装が複雑になるので、クライアント証明書を使用して簡単に済ませたいという要求もあるのではないかと思います。
コロナ対策でセキュリティ強化で2要素認証を実装する際の参考にどうぞ。