ASP.NET Core MVC で実装した Web API を HTTP.sys でホストすることで、利用に Windows 認証が必要な Web API を実現できた。
これでひと段落と思いきや、JWT Bearer 認証もサポートする必要が出てきたので、HTTP.sys でホストした状態で Window 認証と JWT Bearer 認証を両方使えるか試してみた。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; 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.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace HttpSysSample { // アプリケーションの構成 // 本来は application.json か環境変数に持たせるべきだが、 // 今回は簡略化のために static クラスにしておく。 static class AppConfiguration { public const string SiteUrl = "http://localhost:5000"; // JWT の署名で使う秘密鍵 public const string SecretKey = "<your-secret-key>"; } // トークンを取得するために渡された認証情報を格納する 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; } } [ApiController] [Route("api/[controller]")] public class HomeController : ControllerBase { // 匿名で利用できる API [HttpGet("anonymous")] public IActionResult Anonymous() { var user = User.Identity; return Ok(new { user.Name, user.IsAuthenticated, user.AuthenticationType, }); } // Windows 認証が必要な API [HttpGet("windows")] [Authorize] public IActionResult Windows() { var user = User.Identity; return Ok(new { user.Name, user.IsAuthenticated, user.AuthenticationType, }); } // アクセストークンを取得するための API [HttpPost("token")] public IActionResult Token([FromBody]TokenInputModel inputModel) { if (ModelState.IsValid) { // 簡略化のためユーザー名とパスワードは固定 if (inputModel.UserName == "admin" && inputModel.Password == "admin123") { var token = CreateJwtSecurityToken(inputModel.UserName); return Ok(new TokenViewModel { Token = new JwtSecurityTokenHandler().WriteToken(token), Expiration = token.ValidTo, }); } } return BadRequest(); } JwtSecurityToken CreateJwtSecurityToken(string userName) { // JWT に含めるクレーム var claims = new List<Claim>() { // JwtBearerAuthentication 用 new Claim(JwtRegisteredClaimNames.Jti, userName), new Claim(JwtRegisteredClaimNames.Sub, userName), // User.Identity プロパティ用 new Claim(ClaimTypes.Sid, userName), new Claim(ClaimTypes.Name, 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; } // アクセストークンが必要な API [HttpGet("jwt-bearer")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public IActionResult JwtBearer() { var user = User.Identity; return Ok(new { user.Name, user.IsAuthenticated, user.AuthenticationType, }); } } public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // Authorize 属性を付けたアクションだけ認証必須にしたい場合は // HTTP.sys 用の認証スキーマを登録しておく必要がある。 services.AddAuthentication(HttpSysDefaults.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)) }; }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(); } } public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseHttpSys(options => { // Windows 認証を有効にする options.Authentication.Schemes = AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate; // Authorize 属性を付けたアクションだけ認証必須にしたいので、 // 匿名アクセスを許可する options.Authentication.AllowAnonymous = true; }); } }
Web ブラウザを使って手作業で JWT Bearer 認証を試すのは面倒なので、コンソールアプリケーションを書いてみた。HttpClient で Window 認証必須の Web API を利用するには、HttpClientHandler.UseDefaultCredentials を true にすればみたいだ。
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; namespace SampleClient { class Program { static void Main(string[] args) { MainAsync().GetAwaiter().GetResult(); Console.WriteLine("Press Enter Key"); Console.ReadLine(); } async static Task MainAsync() { var handler = new HttpClientHandler() { UseDefaultCredentials = true, }; var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost:5000"), }; // 匿名アクセス可能な API を呼び出す Console.WriteLine( await client.GetStringAsync("/api/home/anonymous")); // Windows 認証が必要な API を呼び出す Console.WriteLine( await client.GetStringAsync("/api/home/windows")); // JWT Bearer 認証が必要な API を呼び出す // アクセストークンを Authorize ヘッダーで指定していないので、 // 呼び出しには失敗する try { Console.WriteLine( await client.GetStringAsync("/api/home/jwt-bearer")); } catch (Exception ex) { Console.WriteLine(ex.Message); } // アクセストークンを取得 var response = await client.PostAsync( "/api/home/token", new StringContent( content: @"{ ""userName"":""admin"", ""password"":""admin123"" }", encoding: Encoding.UTF8, mediaType: "application/json")); var json = await response.Content.ReadAsStringAsync(); var jObj = JObject.Parse(json); var token = (string)jObj["token"]; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // JWT Bearer 認証が必要な API を呼び出す Console.WriteLine( await client.GetStringAsync("/api/home/jwt-bearer")); } } }
実行結果は次の通り。 Window 認証と JWT Bearer 認証、それぞれ期待通りに動いていることを確認できた。