以前、ASP.NET Core MVC で Basic 認証を行う記事を書いた。
プライベートな Web API なのでこれでいいかなと思っていたが、 大人の事情でそうはいかなくなり、 JWT(Json Web Token) を使った認証に変えることに。
Microsoft.AspNetCore.Authentication.JwtBearer パッケージを使って実装できそうだったので、 サンプルを書いてみた。
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Security.Claims; using System.Text; namespace JwtSample { // アプリケーションの構成 // 本来は application.json か環境変数に持たせるべきだが、 // 今回は簡略化のために static クラスにしておく。 public static class AppConfiguration { public static string SiteUrl { get; } = "http://localhost:5000"; // JWT の署名で使う秘密鍵 public static string SecretKey { get; } = Guid.NewGuid().ToString(); } public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .UseApplicationInsights() .UseUrls(AppConfiguration.SiteUrl) .Build(); host.Run(); } } public class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app.UseJwtBearerAuthentication(new JwtBearerOptions { // 着信したトークンの意図された受信者、またはトークンがアクセスを許可 // するリソースを表す。 // このパラメータで指定された値がトークンの aud パラメータと一致しない // 場合、トークンは別のリソースにアクセスするために使用されたため、 // 拒否される。 Audience = AppConfiguration.SiteUrl, // トークンによって定義されたユーザーが自動的にログインするかどうか AutomaticAuthenticate = true, TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateIssuer = true, ValidIssuer = AppConfiguration.SiteUrl, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(AppConfiguration.SecretKey)), } }); app.UseMvc(); } } // ユーザー public class User { public string Id { get; set; } public string UserName { get; set; } public string Password { get; set; } } // パスワードを除いたユーザー情報を格納する public class UserViewModel { public string Id { get; set; } public string UserName { get; set; } } // トークンを取得するために渡された認証情報を格納する public class TokenInputModel { [Required] public string UserName { get; set; } [Required] public string Password { get; set; } } // 生成したトークンと有効期限を格納する public class TokenViewModel { public string Token { get; set; } public DateTime Expiration { get; set; } } [Route("api")] public class HomeController : Controller { // テスト用ユーザー static List<User> _testUsers = new List<User>() { new User() { Id = Guid.NewGuid().ToString(), UserName = "tnakamura", Password = "test1234", } }; [HttpPost("token")] public IActionResult Token([FromBody]TokenInputModel inputModel) { if (ModelState.IsValid) { var user = _testUsers.FirstOrDefault(u => u.UserName == inputModel.UserName); if (user != null && user.Password == inputModel.Password) { var token = CreateJwtSecurityToken(user); return Ok(new TokenViewModel { Token = new JwtSecurityTokenHandler().WriteToken(token), Expiration = token.ValidTo, }); } } return BadRequest(); } JwtSecurityToken CreateJwtSecurityToken(User user) { // JWT に含めるクレーム var claims = new List<Claim>() { // JwtBeaerAuthentication 用 new Claim(JwtRegisteredClaimNames.Jti, user.Id), new Claim(JwtRegisteredClaimNames.Sub, user.UserName), // User.Identity プロパティ用 new Claim(ClaimTypes.Sid, user.Id), new Claim(ClaimTypes.Name, user.UserName), }; var token = new JwtSecurityToken( issuer: AppConfiguration.SiteUrl, audience: AppConfiguration.SiteUrl, claims: claims, expires: DateTime.UtcNow.AddDays(7), signingCredentials: new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppConfiguration.SecretKey)), SecurityAlgorithms.HmacSha256 ) ); return token; } [Authorize] [HttpGet("me")] public IActionResult Me() { var id = User.Claims.First(c => c.Type == ClaimTypes.Sid).Value; var user = _testUsers.First(u => u.Id == id); return Ok(new UserViewModel { Id = user.Id, UserName = user.UserName, }); } } }
curl を使って動作を確認してみる。 まずは JWT を取得せずに、認証が必要な Web API を呼び出してみると、当然ながら 401 が返ってくる。
$ curl -i -s http://localhost:5000/api/me HTTP/1.1 401 Unauthorized Date: Fri, 04 Aug 2017 04:47:20 GMT Content-Length: 0 Server: Kestrel WWW-Authenticate: Bearer
次は JWT を取得。
$ curl -i -s -H 'Content-Type: application/json' --data '{ "userName": "tnakamura", "password": "test1234" }' http://localhost:5000/api/token HTTP/1.1 200 OK Date: Fri, 04 Aug 2017 04:49:44 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTAyNDI2OTg0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.HlDiTrEvEeSjf4a8oAsVAJS-h5BZNPxCf4hBLkNVR8E","expiration":"2017-08-11T04:49:44Z"}
取得した JWT を Authorization ヘッダーに指定して、再び認証が必要な Web API を呼び出すと成功する。
$ curl -i -s -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTAyNDI2OTg0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.HlDiTrEvEeSjf4a8oAsVAJS-h5BZNPxCf4hBLkNVR8E' http://localhost:5000/api/me HTTP/1.1 200 OK Date: Fri, 04 Aug 2017 04:51:29 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked {"id":"4f911c42-9c3c-40b3-bce4-3356407024ea","userName":"tnakamura"}
JWT を使った認証のメドは立ったかな。