3月第4週の振り返り

3月第4週(3/16-3/22)

ASP.NET Core MVC

MSDNのドキュメントを読んでいて学んだ、Dependency InjectionのAddScopedの正確な挙動と、DIを用いて疎結合にすべきとされていた意外なものについて書く。

AddTransientは供給要求のたびに新しいインスタンスが生成される。ステートレスで軽量なサービス(ここでは供給する実装クラスのことを指すと考えられる)に向くとされる。

一方でAddScopedはクライアントのリクエストごとに生成されると書いてあり、これだけではTransientとの区別がつけづらい(Transientであっても、リクエストごとに生成されるControllerインスタンスの中で、供給されたサービスを使い捨てず変数に格納して利用することが主であろう)。正確にはscopedの名の通りスコープの中で同一のインスタンスが供給される。ここでのスコープとはブレースに囲まれたブロックではなくusingステートメントにおいて

using(var scope = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>().CreateScope()){
...
}

で生成されたscopeである。scope.ServiceProvider.GetService<IYourInterface>();で要求すると毎回同一のものが供給される。

ASP.NET Core MVCではリクエストの処理に際してAddScopedの対象となるサービスを自動的にこのscopeを通じ供給するようにしているため、外見上リクエストごとに生成されているとみなせるようである。

DbContextはAddDbContextにおいてScopedな供給のされ方をしているので、先週の記事で述べたリポジトリクラスもAddScopedで供給するのがよい*1


このドキュメントではDateTime.NowをControllerのメソッド中で直接使うことを戒めている。それはテストするタイミングによってそれが変動してしまい、常に同じ結果が得られるとは限らないからである。記事を予約投稿する(指定時刻まではアクセスしてもNot Foundにする)機能を持ったブログアプリケーションを考えたとき、当然指定時刻以前と以後で違うレスポンスが得られるかテストするだろう。

アクセスをController内部のDateTime.Nowで制御すると、テスト時刻の前に公開時刻をセットしたものは見られて逆は404になるよう期待するため、モック記事モデルの公開時刻をテストのたびに書き直すか、あるいはテストコード中でDateTime.Nowから足し引きすることになる。任意の時刻を設定できるモックを基準にするならまだよいが、Controllerの中で直接「日曜日だけ…」などとやっていたとしたら自動テストによる動作保証は絶望的である。

そこでこの記事では抽象化した時刻提供サービスを供給することが推奨されている。単体テスト時には都合の良い時刻を返すモック時刻提供サービスを供給することで常にリクエスト発生時刻を任意のタイミングに指定(仮定)できるというわけである。

これまでDependency Injectionを拡大する方向で進んできたが、それはリポジトリにおけるデータベースコンテキストのように事情が変われば交換可能な依存先について適用すべきもので、帰属関係のあるものにまで適用すべきでないということが言われている。大規模なアプリケーションの経験がないためどういったところで内部newのままにすべきなのかつかめていないが留意しておきたい。

*1:

ASP.NET - Writing Clean Code in ASP.NET Core with Dependency Injection | Microsoft Docs "Scoped is appropriate because my DinnerRepository depends on a DbContext, which also uses the Scoped lifetime."

3月第3週の振り返り

3月第3週(3/9-3/15)

ASP.NET Core MVC

今週はリポジトリパターンについて学んだ。Visual Studioを使ってASP.NET Core MVCで開発している場合、自動生成されるControllerコードでは生成時に指定したDbContextがコンストラクタで供給される(DI)ことになっている。しかしControllerが直接EF Coreの機能を操作するとなると単体テストが難しく、またDbContextに含まれるすべてのテーブルに対してFromSqlRaw()操作が可能であるという過度の権限付与状態にある。

そこでDBのテーブルまたは集約(Aggregate: エンティティとそれに関連するオブジェクト)に対するいくつかの定型的な操作をメソッドとして提供するリポジトリクラス(のインタフェース)をControllerに与えることで、単体テスト時にはモックリポジトリを通じてDBへのアクセスなしに仮のデータで検証することができるし、設計段階でDB操作を制限することが可能となる。

しかし「ある条件に該当するエンティティのうち、ナビゲーションプロパティ(=別テーブル)が特定の条件を満たしているようなもの」といった複雑なクエリをリポジトリで提供しようとすると特殊ケースのためだけのメソッドが増えてしまう。このような場合は(副作用のない)クエリ組み立てを提供するサービスクラスを通じてControllerが制約付きのEF Core操作を実現できるようにすることが一つの手段として挙げられていた。

Dependency Injectionやリポジトリパターンなどこのところ学んだ概念は総じて、ストレートな実装よりも手間は増えるが、のちの検証や修正を容易にし開発者のミスを未然に防ぐための発想なのだと感じた。

3月第2週の振り返り

3月第2週(3/2-3/8)

ASP.NET Core MVC

今週も引き続き環境変数等の扱いを調査した。なかなか複雑で混乱しやすいところであり、ASP.NET特有の部分も多いため時間を要した。

ASP.NET Core 3.1においてWeb MVCアプリケーションを作成すると、ProgramクラスCreateHostBuilder(string[] args)メソッドにおいてHost.CreateDefaultBuilder(args).ConfigureWebHostDefaults(...)が返されるようになっている。

docs.microsoft.com

上記ドキュメントで説明されているように、CreateDefaultBuilderでは

  1. 各種環境変数を読み込む
  2. DOTNET_で始まる環境変数を参照し設定を行う

等の処理を行う。DOTNET_接頭辞のある環境変数は接頭辞なしでもアクセス可能になる*1("DOTNET_abc"でも"abc"でもよい)。

次にConfigureWebHostDefaultsでは

  1. ASPNETCORE_で始まる環境変数を参照し設定を行う
  2. ASPNETCORE_FORWARDEDHEADERS_ENABLEDがtrue/1であればx-forwarded-forを有効にするミドルウェアを自動追加する

等の処理を行う。ASPNETCORE_接頭辞のある環境変数が接頭辞なしでもアクセス可能になる("ASPNETCORE_xyz"でも"xyz"でもよい)。

Startupクラス(Startup.cs)の中ではIConfiguration型 Configurationが供給(DI)される。Configurationはkey-valueセットなのでキー名を指定してConfiguration["pqr"]で環境変数を取得することができる。

では開発環境において同一名の環境変数は―すなわち様々な場所に書き込まれた環境変数は―どの順番で優先されるのだろうか。後に読み込まれたものが優越するのは明らかだが、接頭辞なしでアクセスできるようになるとされているDOTNET_xyzやASPNETCORE_xyzはどこに位置するのか。Configuration["xyz"]の値を表示させる実験の結果は以下である。

  1. launchSettings.jsonのprofiles:【IIS Expressなど】:environmentVariables:xyz(優先)
  2. マシン環境変数のxyz
  3. secrets.jsonのxyz
  4. appsettings.Development.jsonのxyz
  5. appsettings.jsonのxyz

  1. launchSettings.jsonの(略):environmentVariables:ASPNETCORE_xyz
  2. マシン環境変数ASPNETCORE_xyz
  3. ...中略...

  1. launchSettings.jsonの(略):environmentVariables:DOTNET_xyz
  2. マシン環境変数DOTNET_xyz(劣後)
  3. ...後略...

このことから、同一接頭辞キーについて書き込み場所による優先順位は1.から5.で示された通りであり、接頭辞の優先度は接頭辞なし>ASPNETCORE_>DOTNET_ということが分かる。接頭辞の違いがまず考慮され、同一接頭辞中では書き込み場所によって順位が定められている。

Configuration["ASPNETCORE_xyz"]など接頭辞付きでアクセスした場合は、フルネームなので当然完全一致するキーの中だけで読み込み順に従って値が決定される。

結論として外部にプロジェクトコードが公開されても安全が保たれ、かつ環境ごとに異なる値に設定できるようにするためのシークレット書き込み場所は

ということになろう。結局全部マシン環境変数である。Windows Visual StudioについてはVS起動時にマシン環境変数が読み込まれて維持され、VSの起動中は再読み込みされないようなので、頻繁に書き換えて挙動をチェックしたり、あるいは開発環境用に多数存在する設定を社内で簡単に共有したりするにはsecrets.jsonを使うといったところだろうか。

DOTNET_/ASPNETCORE_接頭辞つきのものを省略して参照するのは予期せぬオーバーライドを引き起こしかねないのでやめておいたほうがよさそうだ。CreateDefaultBuilderによるプロバイダをクリアしてConfigurationBuilder().AddEnvironmentVariables(prefix: "YOURPREFIX_")を利用することもできるが、最初からYOURPREFIX_付きで指定すべきであろう。

2月第4週の振り返り

2月第4週(2/17-2/23)

ASP.NET Core MVC

不適切な操作でリソースが解放されないことが多いHttpClientの正しい利用方法を学んだ。定石であるusingステートメントではなく(実際のところusingを使わないところまで定石化しているらしいが)、DIコンテナでIHttpClientFactoryの実装を供給してもらい自分でCreateClient()する方法と、HttpClientを自分のサービスクラスに供給してもらいさらにそのサービスクラスインスタンスを必要な場所に供給してもらう方法とが紹介されていた。

前者はアクセス先のベースURI不定である場合に便利で、後者は同一ベースURIに複数のリクエストを送信する場合に便利である。

またアクセス先のAPIDoS扱いされないよう、リクエストが500エラーで失敗したらexponential backoffで再試行するように設定する必要があるといった場合に、後者では個々のHttpClientにその設定を施すことなくサービスクラスにあらかじめ設定を書き込んでおけばよい点でも優れている。

docs.microsoft.com

利用の方法は上記ドキュメントに記載されているが、流れとしては

  1. Controllerがサービスクラス実装の供給をインタフェースの形で受ける(AddHttpClient<IMyClient, MyClientImpl>();)
  2. そのサービスクラスはHttpClientの供給を受けている(AddHttpClientがこれも行う)
  3. Controllerはサービスクラスインスタンスに、実装されたメソッドの実行を指示する(_myClientInterface.GetDataFromExternalAPI();)
  4. サービスクラスは供給を受けたHttpClientを使ってHTTPリクエストを行う(_httpClient.SendAsync(request);)

となる。単体テストをする気がないのであれば1.で実装クラスを直接指定してもよい。個人的にはビルトインであるHttpClientのラッパーに過ぎないサービスクラスは単体テスト不要だと思うが、そのサービスクラスに依存するControllerの単体テストは書いておきたいと思った。

またHttpClientの使用にあたっては、Content-Typeヘッダを自由に設定できるわけではないことに注意する必要がある。FormUrlEncodedContent, StringContentなどをHttpRequestMessageのContentに設定しておけば自動的に対応するものが指定されるようだ。

このサービスクラスインジェクションを使ってついにプログラム中でトークJSONを取得しパースすることができた。

有効期限は時刻ではなく時間(秒)で渡されるため、トークン情報クラスのコンストラクタでDateTime.UtcNowを取得してAddSecondsで加算した時刻をユーザデータに移し替えようとしたが、加算が反映されておらずしばらく悩んでいた。デバッガでトークン情報クラスの中身をよく見たところ、直接アクセスするべきでないとしてprivateにしたint expire_inに値が入っていないことが分かった。これをpublic int expire_in { private get; set; }にすることで解決した。

しかしpublic setである以上、IntelliSenseにexpire_inが候補として表示されてしまうので完全とは言い難い。来週はこの解決策を探りつつ、セッションのKVS格納を進めていきたい。

2月第3週の振り返り

2月第3週(2/10-2/16)

ASP.NET Core MVC

SignalR以前にFitbit APIの利用に必要なOAuth2.0を理解していないことに気づき調べまわっていた。授業プロジェクトではNuGetのFitbitクライアントパッケージを利用していたが更新されておらず(当時はASP.NET 4で開発していた)、自前で実装するためにFitbit APIリファレンスを読んだところ以下のようなフローであることが分かった。

  1. ユーザをFitbitの/oauth2/authorizeにアクセスさせる。クエリストリングにはresponse_type=code, client_idおよびscopeとstateを指定する

redirect_uriはFitbit連携アプリの管理画面で単一のものを設定している場合は含める必要がない。複数URIを管理画面で指定している(改行で区切る)場合はそのどれかに完全一致する必要があるようだ。そのためオープンリダイレクトの危険性はなさそうであるが、必要がなければ本番環境用クライアントのリダイレクトURI設定は単一にして、クエリストリングには含めないでおくのがよいだろうと思った。

  1. ユーザが自サイトのアプリケーションによるFitbit個人データへのアクセスをAuthorize(認可)したら、Fitbit側が指定のリダイレクトURI(自サイト)にトークン要求用コードをクエリストリングとして付してユーザをリダイレクトさせる
  2. アプリケーションの側で、このリダイレクトによって発生したGETリクエスhttps://[mysite]/my_callback_uri?code=...からクエリストリングを読み取り、トークン要求用コードを取得する
  3. アプリケーションはFitbitの/oauth2/tokenに対しAuthorization: Basic base64_encode("client_id:client_secret")ヘッダー、client_id=XXXXX, grant_type=authorization_code, redirect_uri, code=トークン要求用コードからなるx-www-form-urlencodedなボディでPOSTリクエストを送る
  4. API利用のためのトークン・リフレッシュトークン・認可された権限リストが入ったJSONレスポンスが返される
  5. (必要な権限が認可されていなければ自サイトからユーザにエラーを出す)
  6. Authorization: Bearer トークン の方式で各種APIにリクエストを送る

これがAuthorization Code Grant Flowと呼ばれる方式であるらしい。この方式ではトークン要求用コードはワンタイム(Fitbitの場合)かつ短寿命、トークンも短寿命であるためセキュリティリスクが少ないとされる。

ただし、リダイレクトをブロックしてしまえば未使用のトークン要求用コードを(短寿命であるとはいえ)取得できてしまう。害意を持った人間はこれを利用し、被害者に3.からステップを始めさせるようなリンク、すなわちhttps://[mysite]/my_callback_uri?code=valid_unused_codeを送るか、あるいは攻撃者が運営するサイトや脆弱性のある第三者掲示板などでimgタグのsrc属性に上記リンクを設定したり、HTTPリクエストを送信するようなJavaScriptを埋め込んだりすることで3.のGETリクエストを発生させること(CSRF)ができてしまう。その結果、4.で攻撃者アカウントデータへのアクセスを被害者に認めることになる。もし私のアプリケーションがFitbitプロフィールの変更機能を提供していたとすれば、被害者が自分のアカウントだと誤解して自分の誕生日や名前を攻撃者のアカウントに設定してしまうことが起きうる。

Fitbitは幸いにもデータを書き込むリクエストが限られており、位置情報を含むアクティビティデータはFitbit端末から公式アプリ経由でしかアップロードされないので致命的ではないとはいえ、攻撃者アカウントへの書き込みを被害者に許可するという構図が一般に危険であるということは認識する必要がある。

これを防ぐため、1.の段階でクエリストリングにstate=ランダムで推測困難な文字列を付すということが行われる。そしてユーザがFitbitの認可画面に飛ぶ前に、自サイト側でユーザに対応するサーバ側セッションにその値を記録しておく。

こうするとFitbit側認可ページにアクセスした本人=攻撃者に対応するサーバ側セッションにしかstateは付かない。Fitbit側はリダイレクトの際codeとともにstateもクエリストリングに付してくれるので、GETリクエストに含まれるstateクエリストリングと、アクセス者に対応するサーバ側セッションに記録されたstateの値とを照合し、合致しなければ4.の処理に移らないという対策を自サイト側で講じることができる。

stateが全員共通だったり、推測可能だったりした場合、アクセス者=被害者に対応するサーバ側セッションのstateと、攻撃者が推測して改変したURL中のstateが一致してしまうので対策にはならない。

またstateをセッションIDそのものにしてしまうと、3.のページ中で外部のjs(bootstrapなど)や画像(広告バナーなど)を使用していた場合、それらを要求する際のリファラにセッションIDが載ってしまうので望ましくない。

人間の害意を想定して対策を講じるにはこのように様々な考慮が必要であり、素直な認可に比べて面倒なものとなることがよく分かる。