先日、ASP.NET Core MVC で JWT を使った認証を実装する記事を書いた。
tnakamura.hatenablog.com
しかし、ASP.NET Core 2.0 では認証まわりがガラッと変わってしまったので、
上の記事にある方法は使えなくなってしまった。
/(^o^)\ナンテコッタイ。
なので、ASP.NET Core 2.0 用のサンプルを書いてみた。
using Microsoft.AspNetCore;
using 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.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
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)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls(AppConfiguration.SiteUrl)
.Build();
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Audience = AppConfiguration.SiteUrl;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidIssuer = AppConfiguration.SiteUrl,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(AppConfiguration.SecretKey))
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
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: Wed, 30 Aug 2017 07:41:55 GMT
Server: Kestrel
Content-Length: 0
WWW-Authenticate: Bearer
アクセストークンを取得。
$ curl -i -s -H 'Content-Type: application/json' --data '{"userName":"tnakamura","password":"test1234"}' http://localhost:5000/api/token
HTTP/1.1 200 OK
Date: Wed, 30 Aug 2017 07:44:14 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTA0NjgzODU1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.dyii4SK204KDYvc-fSc_OVZ36-FAyTaa4dtxradCmQI","expiration":"2017-09-06T07:44:15Z"}
取得したアクセストークンを Authorization ヘッダーに指定して、認証が必要な Web API を呼び出すと成功する。
$ curl -i -s -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTA0NjgzODU1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.dyii4SK204KDYvc-fSc_OVZ36-FAyTaa4dtxradCmQI' http://localhost:5000/api/me
HTTP/1.1 200 OK
Date: Wed, 30 Aug 2017 07:45:23 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked
{"id":"020d725b-763e-44b3-ac9e-4f26c4011608","userName":"tnakamura"}