この記事は「F# Advent Calendar 2017」に参加しています。
Giraffe はかつて ASP.NET Core Lambda という名前で、名前が示す通り ASP.NET Core 向けの関数型プログラミングによるウェブアプリケーションフレームワークです。ここでいう関数型プログラミングは当然 F# のことで、 F# の特徴的な機能であるコンピュテーション式を利用するといい感じに書くことができます。
type MessageModel = { Message : string } let webApp = choose [ GET >=> route "/" >=> razorHtmlView "Index" { Message = "Hello, World!" } setStatusCode 404 >=> text "Not Found" ]
上記のように魚演算子 >=>
を利用して関数(ハンドラー)を連鎖させます。
さて、ここからが本題になるのですが、 ASP.NET Core MVC では、 CSRF 対策として、ビューで Html.AntiforgeryToken()
を呼び、コントローラーのメソッドに ValidateAntiForgeryToken
属性を付与します。
<form> @Html.AntiforgeryToken <input type="submit"> </form>
[Controller] public class MyController : Controller { [HttpPost] [ValidateAntiForgeryToken] public IAsyncResult DoSomething() { // ... } }
Giraffe は関数型プログラミングの作法で前述の魚演算子によるハンドラーの連鎖によりリクエストの処理を行います。この関数がコントローラーに相当するものであるので、 ASP.NET Core MVC のコントローラーそのものは利用しません。そのため、 ValidateAntiForgeryToken
に相当するハンドラーを作成する必要があります。
ASP.NET Core MVC では、 IAntiforgery.ValidateRequestAsync
を呼んで AntiforgeryToken の検証を行います。検証に失敗した場合には、例外が投げられます。したがって IAntiforgery.ValidateRequestAsync
を呼ぶようなハンドラーを作ってあげるだけで、 ValidateAntiForgeryToken
に相当することが実現できます。
let validateAntiforgeryToken : HttpHandler = fun next ctx -> task { let antiforgery = ctx.GetService<IAntiforgery>() do! antiforgery.ValidateRequestAsync(ctx) return! next ctx }
以下のように、他のハンドラーの間に挟んでやれば良いでしょう。
POST >=> validateAntiforgeryToken >=> ...
検証に失敗した場合は例外が投げられると GiraffeErrorHandlerMiddleware
が例外をキャッチしてエラーハンドラーに処理をつなぎます。普通は例外が投げられたときに対応するハンドラーは、 HTTP ステータス 500 で返すものを用意するでしょうから、上記のコードで AntiforgeryToken の検証に失敗した場合は 500 が返ってきます。 400 を返すようにするには、以下のように try-with
で AntiforgeryValidationException
を捕捉してレスポンスを返すようにすれば良いでしょう。
try do! antiforgery.ValidateRequestAsync(ctx) return! next ctx with | :? AntiforgeryValidationException as ex -> let handler = clearResponse >=> setStatusCode 400 >=> text ex.Message let interrupt : HttpFunc = fun _ -> Task.FromResult(None) return! handler interrupt ctx
おまけ
setStatusCode
は int
をとるので、誤って変なステータスコードを返してしまう可能性があります。これを防ぐために HttpStatusCode
列挙型を使いたい場合は、以下のようにシャドーイングしてしまうのが良いと思います。
let setStatusCode : HttpStatusCode -> HttpHandler = LanguagePrimitives.EnumToValue >> setStatusCode ... >=> setStatusCode HttpStatusCode.BadRequest >=> ...