JwtBearerAuthentication を使っている Web API を Swashbuckle.AspNetCore で試す

Swashbuckle.AspNetCore を使うことで、 ASP.NET Core で作成した Web API を Swagger UI 上で試せるようになった。 ただ、大抵の Web API では OAuth2 なり JWT Bearer なりの認証が必要、という風に実装していると思う。 自分の場合は JWT Bearer。

Swagger UI ではそのあたりもサポートしていて、 今回は JwtBearerAuthentication を使っている Web API を Swagger UI 上で試せるか挑戦してみた。

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 Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
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 SwaggerWithJwtBearer
{
    // アプリケーションの構成
    // 本来は application.json か環境変数に持たせるべきだが、
    // 今回は簡略化のために static クラスにしておく。
    public static class AppConfiguration
    {
        public static string SiteUrl { get; } = "http://swagger-with-jwt-bearer.net";

        // JWT の署名で使う秘密鍵
        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>()
                .Build();
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddAuthentication(options =>
            {
                // JwtBearer 認証しか使わないので、
                // JwtBearer をデフォルトにする
                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))
                };
            });

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Title = "Sample API",
                    Version = "v1"
                });

                // Swagger UI で API を試すとき、リクエストヘッダーに
                // JwtBearer トークンを設定できるようにする
                c.AddSecurityDefinition("api_key", new ApiKeyScheme()
                {
                    Description = "Bearer スキームを使用した Authorization ヘッダー. 例: \"Authorization: Bearer {token}\"",
                    Name = "Authorization",
                    In = "header",
                    Type = "apiKey"
                });

                // 設定した Bearer トークンをリクエストに含めるためのフィルタを追加
                c.OperationFilter<SecurityRequirementsOperationFilter>();
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();

            app.UseMvc();

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "SmartStamp API V1");
            });
        }
    }

    class SecurityRequirementsOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var allowAnonymous = context.MethodInfo
                .CustomAttributes
                .Any(x => x.AttributeType == typeof(AllowAnonymousAttribute));
            if (allowAnonymous)
            {
                return;
            }

            var attributes = context.MethodInfo
                .GetCustomAttributes(true)
                .Concat(context.MethodInfo.DeclaringType.GetCustomAttributes(true))
                .Where(x => x is AuthorizeAttribute);
            if (attributes.Any())
            {
                operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
                operation.Security.Add(new Dictionary<string, IEnumerable<string>>
                {
                    ["api_key"] = new string[0]
                });
            }
        }
    }

    // ユーザー
    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",
            }
        };

        // トークンを取得する API
        [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)
        {
            // JWT に含めるクレーム
            var claims = new List<Claim>()
            {
                // JwtBeaerAuthentication 用
                new Claim(JwtRegisteredClaimNames.Jti, user.Id),
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                // User.Identity プロパティ用
                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;
        }

        // 認証が必要な API
        [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,
            });
        }
    }
}

デバッグ実行し、/swagger にアクセスすると Swagger UI が表示される。

f:id:griefworker:20170922113542p:plain

認証必須の API を呼び出すと、401 が返ってくる。

f:id:griefworker:20170922113557p:plain

アクセストークンを取得。

f:id:griefworker:20170922113608p:plain

Swagger UI 上で Authorization ヘッダーの内容を設定できる。

f:id:griefworker:20170922113619p:plain

Authorization ヘッダーにアクセストークンが設定されるようになったので、 認証必須の API を呼び出せるようになった。

f:id:griefworker:20170922113629p:plain

もう Swagger UI の無い Web API 開発には戻れないねぇ。

『Essential Xamarin』を読んだ

Xamarin を扱った技術書もだいぶ増えてきたが、 『Essential Xamarin』はかなり毛色が違う。 それもそのはずで、技術書典で発売された同人誌が元になっていて、 それを加筆修正して商業出版したものが本書。 恥ずかしながら、技術書にも同人誌があるということを知らなかった。 元が同人誌なので、体系的に順序立ててまとまっているわけではない。 WEB+DB PRESSみたいな技術雑誌に近いかな。 収録されている記事がどれも濃かった。

Xamarin.Mac について書かれた記事は初めてお目にかかったかも。 Xamarin で WindowsMac 両対応のアプリの開発を考えていたけど、 正直 macOSアプリ開発をどこから手をつければいいのかさっぱりだった。 本記事ではシンプルなアプリを題材に、 サンプルコードとスクリーンショットを多めに載せてあって、 とっかかりとしてピッタリの記事。 Xamarin.Forms が macOS 対応予定とはいえ、 Xamarin.Mac の知識は必要になるだろうね。

Xamarin の中の人による Xamarin Android SDK の解説記事は、 Xamarin.Android SDK の使い方にとどまらず、 その仕組みにも踏み込んで書いてあった。 Xamarin.Android では Java API を呼び出す C# のラッパーを提供しているわけで、 C# から Java をどうやって呼び出しているのか、 またその逆に Java から C# をどうやって呼び出しているのか、 相互運用について知るのに最適。 Xamarin.Android をやるなら絶対読んでおいた方がいいと思える内容。

とにかく、濃すぎて胸焼けをおこしそうなくらい濃厚な一冊だった。

Essential Xamarin ネイティブからクロスプラットフォームまで モバイル.NETの世界 (技術書典シリーズ(NextPublishing))

Essential Xamarin ネイティブからクロスプラットフォームまで モバイル.NETの世界 (技術書典シリーズ(NextPublishing))

『アオアシ(1)〜(10)』を読んだ

今一番面白いサッカー漫画という触れ込みだったので Kindle でまとめ買い。 確かに、今自分が読んでいる中では一番続きが気になるサッカー漫画かもしれない。

アオアシ』はクラブユースサッカーが舞台。 この年代を扱うサッカー漫画の多くは高校サッカーを舞台にしているから、ユースというのは珍しい。 高校サッカーなら学園ドラマも絡めやすかったりするからね。 ユースでもクラブが提携している高校に行ってるから、学園ドラマをやれないことはないんだけど。

その分、サッカーの本質的なところにしっかり踏み込んでいる印象を受けた。 キーワードを上げるなら、トラップ、トライアングル、コーチング、スペース、オフ・ザ・ボールなどなど。 読んでいてサッカーに詳しくなること請け合い。 丁寧に描いてあるので、ストーリーの進みが遅いのはまぁ仕方ない。

あと、途中で主人公の葦人に衝撃の出来事が。 ネタバレになっちゃうから書くのは自粛するけど、こんな展開になるとは思いも寄らなかった。 怪我とか、そんなありきたりな出来事ではない、とだけ書いておく。 というか、サッカー漫画の主人公にこれをやるのかぁ、無いだろ普通、と最初思った。 これから葦人たちがどう成長していくのか、この先が予想できないし、 そこが楽しみ。

『星野、目をつぶって。(7)』を読んだ

自分を変えたい小早川が、星野の助力を得て体育祭の応援団副団長になり、応援合戦を成功させるために団長とガチなぶつかり稽古(比喩)をやるという、なかなか熱い展開だった。特に騎馬戦で団長に下剋上、もとい一騎打ちを挑む場面はシビれた。小早川はウジウジしがちだが、一度行動しだしたら、そこからの行動力は目を見張るものがあるな。星野の影響なんだろうけど。読んでいて、自分も行動しなきゃなぁと焦った。

山本のハンバーグ

東京の人気ハンバーグ店『山本のハンバーグ』が、福岡市の、それも六本松に出店したという情報を入手したので行ってみた。11時45分くらいに到着したら、既に満席で、待ちが1組。5分くらいでカウンター席に座れたのはラッキーだった。

店の名を冠した『山本のハンバーグ(1780円)』を注文。最初にサラダと野菜ジュースが運ばれてきた。野菜ジュースは苦味が無いどころか甘くて美味い。サラダもドレッシングが好み。期待感アップ。

そしてお待ちかねのハンバーグ。ご飯と味噌汁もすぐに来た。

上に乗っている白いソースは卵とマヨネーズでできているらしい。 ハンバーグは肉肉しくて、見た目以上にボリュームがあった。食べ応えあり。デミグラスソースはコクがあって、ハンバーグだけでなく、 付け合わせのジャガイモ、卵、スパゲティどれにもよく合う。白いソースと一緒に食べると、まろやかさも加わって、これまた良い。 ご飯がお代わり自由なのも嬉しい。 つい食べ過ぎてしまった。

あと、接客が過去最高レベルで丁寧だった。 オープンしてまだ間もないからだろうか。 このクオリティを今後も維持できたら凄いことだ。 1780円とランチでは結構高いが、味と接客にかなり満足したので、 なんの不満もなく支払えた。 これは福岡でも人気出そうだ。

お探しの店舗のページはありませんでした

『からかい上手の高木さん(6)』を読んだ

6巻でも高木さんに勝つために色々策を講ずるが、詰めが甘かったり高木さんが上手(うわて)だったりで、結局勝てない西片くんが微笑ましい。 高木さんの方も、西片くんをからかうためなら周囲に誤解されるのも厭わない姿勢は、ブレがなくて素敵。

というか誤解も何も、もはや既成事実化してしまってそうだ。周囲から見た2人の話とかもいつかやって欲しい。オマケに出てきた北条さんとか気になるし。 むず痒くも優しい世界にほっこり癒されて6巻も満足。満腹。

『かぐや様は告らせたい(6)』を読んだ

6巻では沢山のキャラが登場して賑やかだった。いつもの生徒会メンバーだけでなく、会長の妹の圭、 藤原秘書の妹の萌葉、あと早坂に柏木神。 登場数は過去最高かも。

柏木神、早坂、圭それぞれメインの回があって内容も充実していた。 圭が会長を嫌っているのは、やっぱりいい意味(?)で同族嫌悪なんだろうな。 圭が会長に同じことしたら、 会長も怒るかもしれない。 その会長、5巻の花火のときといい、 今回の月見のときといい、 テンションがキマッているときは最強だな。

次巻は生徒会長選挙で新キャラも登場するし、すでに期待で胸が膨らんでいる。