ASP.NET MVC で IIS に頼らず認証を行う属性を作る(設計編)

いまだに MVC2 でございます。
ASP.NET MVCASP.NETフレームワークをうまいこと拡張しているため、多くの機能が ASP.NET と共通です。違うところはいろいろ語られているので、同じところの1つである認証まわりについて簡単に触れつつ、Basic 認証を行う実装を行います。Basic 認証ができればダイジェスト認証への応用は簡単ですし、SSPI を利用した IC カードやバイオメトリクスによる認証にも対応できるでしょう。もちろん。IIS におまかせするのが一番簡単なので、わざわざ自分で実装する利点はありません。

ASP.NET の認証と承認のシーケンスは、ざっくりとはこんなかんじのはずです。通常、Basic 認証を行うためには web.config で を指定しておき、IIS や Apache に Basic 認証をやってもらいます。

承認の復習

ところで、認証の前に ASP.NET MVC における承認のほうを片付けておきましょう。ASP.NET MVC では web.config の タグ以外に、AuthorizeAttribute 属性によって タグと同じようにユーザまたはロールによってアクセス許可を行うことができます。

// このコントローラのすべてのアクションの承認には、Administrators ロールが必要
[Authorize(Roles="Administrators")]
public class MyController : Controller
{
    // このアクションメソッドは、user1 または user2 でのみ承認される
    [Authorize(Users="user1,user2")]
    public ActionResult Action1()
    {
        /* ... */
    }
}

ASP.NET MVC では特定のアクションメソッドにルーティングされる URL が複数ありえるため、web.config の タグを適切にメンテナンスするよりも属性によって宣言しておくほうがお手軽なのでオススメですね。同様の理由で、今回の認証についても属性でやってしまおうと考えるわけです。*1

やるべきこと

冒頭に書いているように、ASP.NET MVC では ASP.NET の機能をそのまま使っていることが多い設計となっており、AuthorizeAttribute も ASP.NET の標準的な動作……つまり、ユーザ認証結果が HttpContext.User に格納されていることを期待しています。このため、作成する Basic 認証の仕組みでは AuthorizeAttribute による承認の仕組みに先立って、認証結果に従ったユーザを HttpContext.User に設定する必要があります。AuthorizeAttribute はアクションフィルタで最も最初に実行される承認フィルタに属するフィルタですが、ASP.NET MVC ではフィルタ属性に Order というプロパティがあって実行順序を設定することができるようになっており、このプロパティを設定することで「認証フィルタ→承認フィルタ」という順序を踏ませることができます。

認証の前に


認証から承認へのルート以外に必要なことがあります。それは、ユーザ エージェントへ認証の要求を行うことです。ASP.NET MVC では、アクションの実行が確定してからレスポンスの確定が行われるルートが大きく3つあります。*2 例外発生時のルート(c)は別にして、残りの2ルートで認証の要求が必要な場合に、適切なレスポンスを生成しなければなりません。
出力フィルタを通るルート(a)では、出力フィルタを用いてレスポンスの入れ替えが可能ですが、問題になるのは承認フィルタからのエラールート(b)です。承認フィルタの実行では、Order が未指定*3のフィルタが最初に実行され、その後に指定された Order の小さいものから大きいものへと実行されていきます。AuthorizeAttribute は、ユーザ認証が行われていない場合*4や、認証結果で得られたユーザの権限が十分ではなくアクセス承認ができない場合*5には、HttpUnauthorizedRequest を返すようになっています。これは、一般的な認証設定であれば冒頭の図のフローの末尾にあるような IIS や ASP.NET の HttpModule によって処理され、適切な認証要求に変換されます。しかし、今回はその機能を利用できない*6ため、これらのシチュエーションで適切な認証要求を作成する必要があります。
この2つの処理についての実装を考えると、ASP.NET MVC 側で認証と承認を行う場合には、

  • ユーザ認証に必要な情報が提示されていない場合
    • 認証フェーズと承認フェーズ、どちらでも検出できる
    • authentication mode が Windows や Forms の場合、認証フェーズでは匿名アクセスとして扱う
    • 承認フェーズを実装した AuthorizeAttribute には、すでに実装済みだが認証要求は発生しない
  • ユーザ権限が不足している場合
    • 承認フェーズでしか検出できない
    • 承認フェーズを実装した AuthorizeAtribute には、すでに実装済みだが認証要求は発生しない
    • 認証要求を行わず、アクセス不可を返す場合も多い

このような状況になっていると思います。以上を踏まえて実装を行っていきたいと思います。

*1:ここに、すでに無理があります。

*2:アクションメソッド本体の実行や、ビューのレンダリングのための実行は、アクションフィルタや結果フィルタの実行の途中で行われます。

*3:Order が未指定のフィルタは、実行順序に依存しない単独で動作する独立性の高いフィルタであることを意味します。

*4:HttpContext.User が匿名ユーザである場合

*5:AuthenticateAttribute に指定した Users や Roles による制限

*6:専用の HttpModule を作成するのも1つの選択肢です。その場合はアクションメソッド毎に属性で指定できる利点が失われかねないため、認証処理をあわせて HttpModule で実装するほうが良いのではないか?と思います。