LoginSignup
2
2

More than 3 years have passed since last update.

ASP.NET Core3.1でJWT認証の実装(公開鍵、非対称鍵認証)

Last updated at Posted at 2021-03-16

前書き

JWT認証の際、非対称鍵による認証の手順です。
秘密鍵で暗号化し、公開鍵で複合します。

秘密鍵同士による共通鍵方式と違い、公開鍵なので基本誰でも復号できる前提です。

公開鍵で複合することで2つのことが証明されます
・発行元
・トークン改ざんされていないこと

例えば、
認証サーバーのみが秘密鍵を保持していて、
複数の別システムが公開鍵を持っている状況の認証フローを想定します。

  1. 認証サーバーはID/PASSでユーザー認証後と秘密鍵でJWTを生成
  2. 他のシステムはJWTを公開鍵で復号
  3. 発行元が認証サーバーであること、トークンが改ざんされていないことが確認できる
  4. 認証OK

ただし、今から説明するプロジェクトは暗号化と復号化が同じサーバーなので、非対称鍵のメリットはありません。
非対称鍵のJWTのやり方だけ説明します。

環境・使用ツール

・VisualStudio2019
・CMD
・ASP.NET Core

検証時
・Postman

下準備

秘密鍵・公開鍵の生成

SHA256暗号化方式

以下のコマンドをCMDで実行、実行したディレクトリにファイルができます。

ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub 

参考サイト

プロジェクトの作成

・今回はASP.NET CORE3.1のWebAPIで作成しました
image.png

NuGetでライブラリのインストール

・System.IdentityModel.Tokens.Jwt
・Microsoft.AspNetCore.Authentication.JwtBearer
image.png

appsetting.jsonの修正

秘密鍵と公開鍵を設定します(貼り付けるのに少しコツがいるかも。。)

appseting.json
{
  "Jwt": {
    "Asymmetric": {
      "PrivateKey": "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",
      "PublicKey": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAty7lSC0kTsS48PSZXBptuBWj4An57LgunUwnLyAx+IhxHPZqnSOAjwFh2dOC1MVEjWz9GtMlMW4wiIuwsBSvxzMzlnI/9L5VuW5aKU5m34DrR0hwtJE3rCByIxhQ9nbT2Y/hO8epWqh0yNKqQwVIXEJ/jf/5Zv8+UqCKy1BjAQ0BCRr+pzJqNq6VV8PjQb8XPw3glvrnm3hP34NoQAc0qdnNIPZ36eYlJX/oTAZBAdUNbgOaV7piL1mBg/R+sOAOnMvy3plhWyg3WH73dLq/Gg+eqODEUYZ3p+WGw/Vt0FFfdudeWMUTUkR+Go0EVndITkppiYlL3L0WtnHhiGS0B8zr6r8ftR1TVRt+fpE4d4gciIliJ9QP/jn1rwMY7moUrumJv4zRwhSkWBgtn/viRlCOsboZVgP5lmQ9JTJXw1o2NJ/uwU26i9dMtjxtAGGjzTL281eIOpIDQtMUdJ8DKO/3Pecnhf3uBUgONRf21RV/SAyjHac1Jiqase4nYYtzphhLqLhUAkj77W2zf60lNncpszNp+GWoZrl7HJqwjZrQsrT+ep2D5oSb6p3fwR9IyBqcfk8obZMZMJX2iUrpvHi8TMW7/Iw3564kFfj4EVKRZcjxgT5q9CvSSd7ByxZAwXGzFPzqAadu5w0e5SUr88aIbixrr7CldozVwdgybPcCAwEAAQ=="
    },
    "TokenExpiredHours": 24
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

実装

JwtServiceクラスを作成

JwtService
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;


namespace ASPNETCORE_JWT.Services
{

    public class JwtService
    {
        private RsaSecurityKey _rsa { get; }
        private IConfiguration _configuration { get; }
        public JwtService(IConfiguration configuration)
        {
            _configuration = configuration;
            // 公開鍵の設定
            RSA rsa = RSA.Create();
            rsa.ImportSubjectPublicKeyInfo(
                source: Convert.FromBase64String(configuration["Jwt:Asymmetric:PublicKey"]),
                bytesRead: out int _
            );
            _rsa = new RsaSecurityKey(rsa);
        }

        public string GenerateJwt(Guid userId)
        {
            using RSA rsa = RSA.Create();
            rsa.ImportRSAPrivateKey(
                source: Convert.FromBase64String(_configuration["Jwt:Asymmetric:PrivateKey"]),
                bytesRead: out int _);

            var signingCredentials = new SigningCredentials(
                key: new RsaSecurityKey(rsa)
                {
                    // using文でも破棄されないキャッシュがあり
                    // 偶数リクエストで失敗するため、修正されるまで追加
                    CryptoProviderFactory = new CryptoProviderFactory()
                    {
                        CacheSignatureProviders = false
                    }
                },
                algorithm: SecurityAlgorithms.RsaSha256
            );

            var tokenExpiredHours = double.Parse(_configuration["Jwt:TokenExpiredHours"]);

            // JWTのペイロードの中身
            var jwtToken = new JwtSecurityToken(
                audience: "JWT_Client",
                issuer: "JWT_Server",
                claims: new Claim[] {
                    new Claim(ClaimTypes.NameIdentifier, userId.ToString())
                },
                notBefore: DateTime.UtcNow,
                expires: DateTime.UtcNow.AddHours(tokenExpiredHours),
                signingCredentials: signingCredentials
            );

            var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            return token;
        }

        public TokenValidationParameters GetTokenValidationParameters()
        {
            // 公開鍵でトークンの検証
            // JWTの有効期限の検証
            return new TokenValidationParameters
            {
                IssuerSigningKey = _rsa,
                RequireSignedTokens = false,
                RequireExpirationTime = true,
                ValidateLifetime = true,
                LifetimeValidator = LifetimeValidator,
                ValidateAudience = false,
                ValidateIssuer = false,
                CryptoProviderFactory = new CryptoProviderFactory()
                {
                    // using文でも破棄されないキャッシュがあり
                    // 偶数リクエストで失敗するため、修正されるまで追加
                    CacheSignatureProviders = false
                }
            };
        }

        public bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters @params)
        {
            if (expires != null)
            {
                return expires > DateTime.UtcNow;
            }
            return false;
        }
    }
}

リクエストの最初にJWTの検証を挟むように設定します。

Startup.cs
   public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddHttpContextAccessor();

            services.AddSingleton<JwtService>();

            services.AddAuthentication()
              .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
              {
                  var sp = services.BuildServiceProvider();
                  var ssoTokenService = sp.GetRequiredService<JwtService>();
                  options.TokenValidationParameters = ssoTokenService.GetTokenValidationParameters();
              });
        }

検証用のコントローラー追加

LoginController.cs
using ASPNETCORE_JWT.Extentions;
using ASPNETCORE_JWT.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;

namespace ASPNETCORE_JWT.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class LoginController : ControllerBase
    {
        private IHttpContextAccessor _httpContextAccessor { get; }
        private JwtService _jwtService { get; }
        public LoginController(IHttpContextAccessor httpContextAccessor, JwtService jwtService)
        {
            _httpContextAccessor = httpContextAccessor;
            _jwtService = jwtService;
        }

        [HttpPost]
        public string Login()
        {
            var userId = Guid.NewGuid();
            var jwt = _jwtService.GenerateJwt(userId);
            return jwt;
        }

        [HttpGet]
        // Authorize属性を指定したメソッドのみJWTの検証の対象になります。
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        public Guid GetUserId()
        {
            return _httpContextAccessor.GetUserId();
        }
    }
}

JWTの検証を通ったリクエストからUserIdを取得します。

HttpContextAccessorExtension.cs
using Microsoft.AspNetCore.Http;
using System;
using System.Linq;
using System.Security.Claims;

namespace ASPNETCORE_JWT.Extentions
{
    public static class HttpContextAccessorExtension
    {
        public static Guid GetUserId(this IHttpContextAccessor self)
        {
            var nameIdentifier = self.HttpContext.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
            if (nameIdentifier == null)
                throw new Exception("ClaimにIdが含まれていません");

            return Guid.Parse(nameIdentifier.Value);
        }
    }
}

確認

サーバーを起動して、Postmanでリクエストを投げます。

Login

・MethodをPostに変更
image.png
JWTトークンが取得できます。

GetUserId

・MethodをGetに変更
・Authorizationタブに切り替え
・TypeをBearer Tokenに変更
・Loginで取得したJWTを貼り付けてSend
image.png

UserIdが取れることが確認できます。

終わりに

鍵の生成にC#のRSA.Create()を使ったら、内部的にSHA1が使われていたので、
CMDで生成する手順にしてます。

リポジトリ
https://github.com/shinnosukekubo/ASPNETCORE_JWT

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2