はじめに
ASP.NET Core で JWT Bearer 認証を使うときに Startup.ConfigureServices
で呼び出す AddJwtBearer
は、
任意の authenticationScheme を指定できる。
となると、authenticationScheme さえ重複しなければ複数回呼び出しても問題ない、はず。 気になったので実験してみた。
Web API
AddJwtBearer
では、authenticationScheme だけでなく署名に使うキーも変えておく。
JWT Bearer のデフォルト authenticationScheme は Bearer だけど、今回は分かりやすいように Bearer1 と Bearer2 にしてみた。
using System; using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; 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.Hosting; using Microsoft.IdentityModel.Tokens; namespace JwtBearerSample { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } public class Startup { internal static readonly SymmetricSecurityKey Bearer1SecurityKey = new SymmetricSecurityKey(Guid.Parse("FA99A883-1B90-4515-A841-64BE3322A663").ToByteArray()); internal static readonly SymmetricSecurityKey Bearer2SecurityKey = new SymmetricSecurityKey(Guid.Parse("AECD611D-B2DC-4909-B921-8465A9C238A8").ToByteArray()); public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // JWT Bearer 認証を組み込む services.AddAuthentication("Bearer1") .AddJwtBearer("Bearer1", options => { // サンプルを簡略化するため検証機能を OFF にする // 本番でこんなことをしてはダメ options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, IssuerSigningKey = Bearer1SecurityKey }; }) .AddJwtBearer("Bearer2", options => { // サンプルを簡略化するため検証機能を OFF にする // 本番でこんなことをしてはダメ options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, IssuerSigningKey = Bearer2SecurityKey }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); // パイプラインに認証と認可を組み込む app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } public class TokenRequest { [Required] public string UserName { get; set; } } public class TokenResponse { public string Token { get; set; } } // アクセストークンを発行する API [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { // Bearer1 用のアクセストークンを発行する [HttpPost("bearer1token")] public ActionResult<TokenResponse> Bearer1Token([FromBody] TokenRequest request) { var token = GenerateJwtToken(request.UserName, Startup.Bearer1SecurityKey); return new TokenResponse { Token = token, }; } // Bearer2 用のアクセストークンを発行する [HttpPost("bearer2token")] public ActionResult<TokenResponse> Bearer2Token([FromBody] TokenRequest request) { var token = GenerateJwtToken(request.UserName, Startup.Bearer2SecurityKey); return new TokenResponse { Token = token, }; } string GenerateJwtToken(string userName, SecurityKey securityKey) { var claims = new[] { new Claim(ClaimTypes.Name, userName), }; var credentials = new SigningCredentials( securityKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( "ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials); var tokenHandler = new JwtSecurityTokenHandler(); return tokenHandler.WriteToken(token); } } [Route("api/[controller]")] [ApiController] public class GreetingController : ControllerBase { // Bearer1 用のアクセストークンで呼び出せる [Authorize(AuthenticationSchemes = "Bearer1")] [HttpGet("hello")] public string Hello() { return $"Hello {User.Identity.Name}"; } // Bearer2 用のアクセストークンで呼び出せる [Authorize(AuthenticationSchemes = "Bearer2")] [HttpGet("good-morning")] public string GoodMorning() { return $"Good morning {User.Identity.Name}"; } // Bearer1, Bearer2 両方のアクセストークンで呼び出せる [Authorize(AuthenticationSchemes = "Bearer1,Bearer2")] [HttpGet("good-afternoon")] public string GoodAfternoon() { return $"Good afternoon {User.Identity.Name}"; } } }
Client
Bearer1 と Bearer2 それぞれのアクセストークンを使って、保護しているすべての API を呼び出してみる。
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace Client { class Program { static readonly Uri BaseAddress = new Uri("http://localhost:5000"); static async Task Main(string[] args) { await Bearer1Test(); Console.WriteLine(); await Bearer2Test(); Console.ReadLine(); } static async Task Bearer1Test() { Console.WriteLine("== Bearer1 =="); var httpClient = new HttpClient() { BaseAddress = BaseAddress, }; var tokenResponse = await httpClient.PostAsync( "/api/account/bearer1token", new StringContent( @"{ ""userName"": ""Kubo"" }", Encoding.UTF8, "application/json")); var json = await tokenResponse.Content.ReadAsStringAsync(); var jDocument = JsonDocument.Parse(json); var accessToken = jDocument.RootElement.GetProperty("token").GetString(); Console.WriteLine(accessToken); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", accessToken); var helloResponse = await httpClient.GetAsync("/api/greeting/hello"); Console.WriteLine(await helloResponse.Content.ReadAsStringAsync()); var goodMorningResponse = await httpClient.GetAsync("/api/greeting/good-morning"); Console.WriteLine(goodMorningResponse.StatusCode); var goodAfternoonResponse = await httpClient.GetAsync("/api/greeting/good-afternoon"); Console.WriteLine(await goodAfternoonResponse.Content.ReadAsStringAsync()); } static async Task Bearer2Test() { Console.WriteLine("== Bearer2 =="); var httpClient = new HttpClient() { BaseAddress = BaseAddress, }; var tokenResponse = await httpClient.PostAsync( "/api/account/bearer2token", new StringContent( @"{ ""userName"": ""Minamino"" }", Encoding.UTF8, "application/json")); var json = await tokenResponse.Content.ReadAsStringAsync(); var jDocument = JsonDocument.Parse(json); var accessToken = jDocument.RootElement.GetProperty("token").GetString(); Console.WriteLine(accessToken); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", accessToken); var helloResponse = await httpClient.GetAsync("/api/greeting/hello"); Console.WriteLine(helloResponse.StatusCode); var goodMorningResponse = await httpClient.GetAsync("/api/greeting/good-morning"); Console.WriteLine(await goodMorningResponse.Content.ReadAsStringAsync()); var goodAfternoonResponse = await httpClient.GetAsync("/api/greeting/good-afternoon"); Console.WriteLine(await goodAfternoonResponse.Content.ReadAsStringAsync()); } } }
実行結果
Bearer1 用のアクセストークンを使うと、AuthenticationSchemes に Bearer1 を指定している Hello と GoodAfternoon は呼び出せるが、指定していない GoodMorning は呼び出せない。
Bearer2 用のアクセストークンを使うと、AuthenticationSchemes に Bearer2 を指定している GoodMorning と GoodAfternoon は呼び出せるが、指定していない Hello は呼び出せない。
おわりに
予想通り、authenticationScheme を変えて複数回 AddJwtBearer
を呼び出してもちゃんと動くことを確認できた。
十中八九上手くいくと思っていても、実際に動かしてこの目で確認するまでは安心できないからね。