認証スキームの異なる複数の JWT Bearer 認証を構成してみた

はじめに

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());
        }
    }
}

実行結果

f:id:griefworker:20200512145643p:plain

Bearer1 用のアクセストークンを使うと、AuthenticationSchemes に Bearer1 を指定している Hello と GoodAfternoon は呼び出せるが、指定していない GoodMorning は呼び出せない。

Bearer2 用のアクセストークンを使うと、AuthenticationSchemes に Bearer2 を指定している GoodMorning と GoodAfternoon は呼び出せるが、指定していない Hello は呼び出せない。

おわりに

予想通り、authenticationScheme を変えて複数回 AddJwtBearer を呼び出してもちゃんと動くことを確認できた。 十中八九上手くいくと思っていても、実際に動かしてこの目で確認するまでは安心できないからね。