この記事は「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 >=> ...