ASP.NET Core 3.0 アプリケーションで JWT を使用する

このページは、ASP.NET Core 2.0 アプリケーションを JWT でセキュアする (Auth0) の記事をベースに、ASP.NET Core 3.0 で JWT を使用する例を示すページです。

注意

  • ASP.NET Core 3.0 の API アプリケーションを新規作成したときに追加されているサンプルコントローラ (WeatherForecastController) を JWT を使って認証付きにするものです。
  • API へのアクセスを認証する までの流れを試しています。

Startup.cs

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.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; // [追加]

namespace JWTAuthApi1
{
    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)
        {
            // [追加] https://auth0.com/blog/jp-securing-asp-dot-net-core-2-applications-with-jwts/ と同じように認証設定をセットします。
            // 「JwtBearerDefaults」はそのままだとエラーになるので、エラーの下線にカーソルを合わせたときに出てくるヒントアイコンから
            // 「パッケージ 'Microsoft.AspNetCore.Authentication.JwtBearer' のインストール」 > 「最新バージョンのインストール」で導入します。
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => 
            {
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                };
            });

            services.AddControllers();
        }

        // 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.UseAuthentication(); // [追加] 認証を使用するようにします。

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

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

appsettings.json

appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
    "Issuer": "https://example.com",
  }
}
  • "Jwt" の設定を追加しています。
  • Key は短すぎるとトークン生成処理の時 (後述の TokenController BuildToken() 時) にエラーになります。16文字以上あるとエラーにならないようです。
  • Issuer は稼働するサービスの URL にするとよいですが、特に制約はありません。 ("Issuer": "Issuer" でもいい)
Key が短すぎる場合のエラー
System.ArgumentOutOfRangeException: 'IDX10603: Decryption failed. Keys tried: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Exceptions caught:
 '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' '

Controller/WeatherForecastController.cs (サンプルとして最初から追加されているコントローラ)

Controller/WeatherForecastController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; // [追加]
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace JWTAuthApi1.Controllers
{
    [Authorize] // [追加] 認証を必要にします。([Authorize])
    [ApiController]
    [Route("api/[controller]")] // [変更] 必要に応じてエンドポイントを変更します
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [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();
        }
    }
}
  • もともとのこのコントローラのルート設定は [Route("[controller]")] ですが、変えたほうがわかりやすい気がします。(下記の理由)
    • コントロールの追加 (Controllers フォルダを右クリック > 追加 > コントローラ) で追加したコントローラの初期のルート設定は [Route("api/[controller]")]
    • この後追加するトークン発行用のコントローラ (Auth0の記事に記載) のルート設定も [Route("api/[controller]")]

実行したときに最初に表示されるURLは、Properties/launchSettings.json の「launchUrl」の設定を変更することで変更できます。

Properties/launchSettings.json (変更例)
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:51735",
      "sslPort": 44362
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/weatherforecast",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "JWTAuthApi1": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/weatherforecast",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Controller/TokenController.cs (トークン発行用のコントローラ。新規追加)

Auth0の記事に記載のある、トークン発行用のコントローラです。

Controllers/TokenController.cs
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace JWTAuthApi1.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class TokenController : ControllerBase
    {
        private IConfiguration _config;
        public TokenController(IConfiguration config)
        {
            _config = config;
        }

        [AllowAnonymous]
        [HttpPost]
        public IActionResult CreateToken([FromBody]LoginModel login)
        {
            IActionResult response = Unauthorized();
            var user = Authenticate(login);

            if (user != null)
            {
                var tokenString = BuildToken(user);
                response = Ok(new { token = tokenString });
            }

            return response;
        }
        private string BuildToken(UserModel user)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(_config["Jwt:Issuer"],
              _config["Jwt:Issuer"],
              expires: DateTime.Now.AddMinutes(30),
              signingCredentials: creds);

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
        private UserModel Authenticate(LoginModel login)
        {
            UserModel user = null;

            if (login.Username == "mario" && login.Password == "secret")
            {
                user = new UserModel { Name = "Mario Rossi", Email = "mario.rossi@domain.com" };
            }
            return user;
        }
        public class LoginModel
        {
            public string Username { get; set; }
            public string Password { get; set; }
        }

        private class UserModel
        {
            public string Name { get; set; }
            public string Email { get; set; }
            public DateTime Birthdate { get; set; }
        }
    }
}

確認

下記を確認して、問題なければ OK です。

  • 実行してみて /api/weatherforecast にアクセスした時、ブラウザに 401 エラーが表示される
  • API へのアクセスを認証する のように Postman や cURL などで /api/token にアクセスしてみてトークンが取得できる
  • 取得したトークンをもとに Postman や cURL などで /api/weatherforecast にアクセスしてみて JSON の応答がある