以前、ASP.NET Core MVC で Basic 認証を行う記事を書いた。
tnakamura.hatenablog.com
プライベートな 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
{
public static class AppConfiguration
{
public static string SiteUrl { get; } = "http://localhost:5000";
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
{
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)
{
var claims = new List<Claim>()
{
new Claim(JwtRegisteredClaimNames.Jti, user.Id),
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
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 を使った認証のメドは立ったかな。