認証に ASP.NET Core Identity を使うけど、アプリ向けに JwtBearer の認証もサポートしたい。JwtBearer でトークンを発行するときは、ユーザーを認証するコードを書くことになるわけなので、そこで Identity の SignInManager や UserManager を使えばいいかもしれない。早速試してみた。
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
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.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace IdentityJwtSample
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var dict = new Dictionary<string, string>
{
["ConnectionStrings:DefaultConnection"] = "Server=(localdb)\\mssqllocaldb;Database=aspnet-IdentityJwtSample-550CDEDB-AB81-4711-8A92-0988E2505FD1;Trusted_Connection=True;MultipleActiveResultSets=true",
["JwtKey"] = "9AC93E2B-8C4F-46C3-B9E5-F7E7CB37A73A",
["JwtIssuer"] = "http://yourdomain.com",
};
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(dict);
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseConfiguration(builder.Build())
.Build();
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
};
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext dbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
dbContext.Database.EnsureCreated();
}
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
public class ApplicationUser : IdentityUser
{
}
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public class TokenViewModel
{
public string Token { get; set; }
}
public class UserViewModel
{
public string Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
[Route("api/user")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IConfiguration _configuration;
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IConfiguration configuration)
{
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser()
{
UserName = model.Email,
Email = model.Email
};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
var token = await GenerateJwtTokenAsync(user);
return Ok(token);
}
AddErrors(result);
}
return BadRequest(ModelState);
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody]LoginViewModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, lockoutOnFailure: false);
if (result.Succeeded)
{
var user = await _userManager.FindByEmailAsync(model.Email);
var token = await GenerateJwtTokenAsync(user);
return Ok(token);
}
}
return BadRequest(ModelState);
}
async Task<TokenViewModel> GenerateJwtTokenAsync(ApplicationUser user)
{
var principal = await _signInManager.CreateUserPrincipalAsync(user);
var claims = new List<Claim>(principal.Claims);
claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.UserName));
claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtKey"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(7);
var token = new JwtSecurityToken(
_configuration["JwtIssuer"],
_configuration["JwtIssuer"],
claims,
expires: expires,
signingCredentials: credentials
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return new TokenViewModel()
{
Token = jwt
};
}
[HttpGet("current")]
[Authorize]
public async Task<IActionResult> Current()
{
var user = await _userManager.GetUserAsync(User);
var viewModel = new UserViewModel()
{
Id = user.Id,
UserName = user.UserName,
Email = user.Email,
};
return Ok(viewModel);
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
}
curl で動作確認してみる。
まずはユーザー登録。
ユーザーの登録に成功し、アクセストークンが返ってきた。
次はログイン。
ログインに成功すればアクセストークンが返ってくる。
そのアクセストークンを使って、認証必須の API を呼び出せるかテスト。
ログインユーザーの情報が返ってきた。テスト成功。