ASP.NET

ASP.NET Web API のサービス セキュリティを有効にしてカスタマイズする

Peter Vogel

同じサイト上の Web API サービスにアクセスする Web ページの JavaScript という最も一般的なシナリオでは、ASP.NET Web API のセキュリティについての議論はほとんど無意味です。ユーザーを認証し、サービスを利用する JavaScript が含まれた Web フォーム/ビューへのアクセスを承認するのであれば、おそらくサービスに必要なすべてのセキュリティを提供しているでしょう。これは、Cookie 情報と認証情報を送信する ASP.NET の働きによるものです。ASP.NET は、これらの情報を使用して、サービス メソッドに対するクライアント側 JavaScript 要求の一部としてページ要求を検証します。1 つ (そして重要な) 例外があり、ASP.NET では、クロスサイト リクエスト フォージェリ (CSRF/XSRF) 攻撃を自動的に防ぐことはできません (後ほど詳しく説明します)。

CSRF 以外にも、Web API サービスのセキュリティ保護についての議論に意味があるシナリオが 2 つあります。1 つ目は、ApiController と同じサイトのページ以外のクライアントによって、サービスが使用されるシナリオです。おそらくこうしたクライアントは、フォーム認証によって認証されておらず、ASP.NET がサービスへのアクセス制御に使用する Cookie とトークンを取得していません。

2 つ目のシナリオは、ASP.NET セキュリティから提供されているレベルを超えた承認をサービスに追加する場合に発生します。ASP.NET から提供される既定の承認は、認証時に ASP.NET によって要求に割り当てられる ID に基づいています。この ID は、ID の名前やロール以外の要素に基づいてアクセスを承認するように拡張できます。

Web API には、この両方のシナリオに対処する選択肢が多数用意されています。実のところ、この記事では Web API 要求を受け取る場合のセキュリティについて説明しますが、Web API は Web フォームや MVC と同じ ASP.NET の基盤に基づいているので、この記事で取り上げるツールは、Web フォームや MVC のセキュリティ内部を扱った経験がある方にはおなじみでしょう。

1 つ注意点があります。Web API には認証と承認の方法が複数用意されていますが、セキュリティの出発点はホスト (IIS、または自己ホストを行う際に作成するホスト) です。たとえば、Web API サービスとクライアントの間の通信におけるプライバシーを確保する場合は、最低でも SSL を有効にする必要があります。ただし、これは、開発者ではなくサイト管理者の仕事です。この記事では、開発者が Web API サービスをセキュリティ保護するためにできること (およびする必要があること) に焦点を置くために、ホストについての説明は省略します (ここで説明するツールは、SSL が有効でも無効でも機能します)。

クロスサイト リクエスト フォージェリ攻撃を防ぐ

ユーザーがフォーム認証を使用して ASP.NET サイトにアクセスすると、ASP.NET によって、ユーザーが認証されていることを保証する Cookie が生成されます。ブラウザーでは、(要求の発生元にかかわらず) 以降の要求を行うたびに、この Cookie がサイトに送信され続けます。このため、サイトは CSRF 攻撃を受けやすくなります。この状況は、ブラウザーで以前受け取った認証情報を自動的に送信する認証方式と同様です。サイトからブラウザーにセキュリティ Cookie が送信された後でユーザーが悪意のあるサイトにアクセスすると、そのサイトはブラウザーで既に受け取っている認証 Cookie に便乗して、サービスに要求を送信できるようになります。

CSRF 攻撃を防ぐには、サーバーで偽造防止トークンを生成してページに組み込み、クライアント側の呼び出しで使用する必要があります。マイクロソフトが提供する AntiForgery クラスには、要求を行ったユーザーに固有のトークンを生成する GetToken メソッドが用意されています (もちろん、ユーザーは匿名ユーザーでもかまいません)。次のコードでは、2 つのトークンを生成し、トークンをビュー内で使用できる ASP.NET MVC の ViewBag に格納します。

[Authorize(Roles="manager")]
 public ActionResult Index()
 {
   string cookieToken;
   string formToken;
   AntiForgery.GetTokens(null, out cookieToken, out formToken);
   ViewBag.cookieToken = cookieToken;
   ViewBag.formToken = formToken;
   return View("Index");
 }

サーバーに対するすべての JavaScript 呼び出しでは、要求の一部としてトークンを返す必要があります (CSRF サイトではこうしたトークンを保有していないため、トークンを返すことができません)。ビュー内の以下のコードでは、要求のヘッダーにトークンを追加する JavaScript 呼び出しを動的に生成します。

$.ajax("http://phvis.com/api/Customers",{
   type: "get",
   contentType: "application/json",
   headers: {
     'formToken': '@ViewBag.formToken',
     'cookieToken': '@ViewBag.cookieToken' }});

もう少し複雑なソリューションでは、ビュー内の非表示フィールドにトークンを埋め込むことで、控えめな JavaScript を使用できます。この手法の最初の手順は、ViewData 辞書にトークンを追加することです。

ViewData["cookieToken"] = cookieToken;
 ViewData["formToken"] = formToken;

これで、ビュー内の非表示フィールドにデータを埋め込めるようになりました。HtmlHelper の Hidden メソッドで必要な処理は、input タグを正しく生成するために ViewDate 内のキーの値を渡すことだけです。

@Html.Hidden("formToken")

生成した input タグでは、タグの name 属性と id 属性に ViewData キーが使用され、タグの value 属性に ViewData 辞書から取得したデータが指定されます。先ほどのコードから生成した input タグは、次のようになります。

<input id="formToken" name="formToken" type="hidden" value="...token..." />

これで、(ビューの個別のファイルに保存されている) JavaScript コードで input タグから値を取り出して、ajax 呼び出しで使用できます。

$.ajax("http://localhost:49226/api/Customers", {
   type: "get",
   contentType: "application/json",
   headers: {
     'formToken': $("#formToken").val(),
     'cookieToken': $("#cookieToken").val()}});

この処理は、(ページの ClientScript プロパティから取得した) ClientScriptManager オブジェクトの RegisterClientScriptBlock メソッドを使用して、トークンが埋め込まれた JavaScript を挿入すれば、ASP.NET Web フォーム サイトで実行することもできます。

string CodeString = "function CallService(){" +
   "$.ajax('http://phvis.com/api/Customers',{" +
   "type: 'get', contentType: 'application/json'," +
   "headers: {'formToken': '" & formToken & "',” +
   "'cookieToken': '" & cookieToken & "'}});}"
 this.ClientScript.RegisterClientScriptBlock(
   typeOf(this), "loadCustid", CodeString, true);

最後に、JavaScript 呼び出しによってトークンが返されたら、そのトークンをサーバーで検証する必要があります。ASP.NET および Web Tools 2012.2 更新プログラムを適用している Visual Studio 2012 ユーザーは、新しい単一ページ アプリケーション (SPA) テンプレートに、Web API メソッドで使用できる ValidateHttpAntiForgeryToken フィルターが付属していることにお気付きでしょう。このフィルターがなければ、トークンを取得して AntiForgery クラスの Validate メソッドに渡す必要があります (トークンが有効でない場合、または異なるユーザーに対して生成されている場合は、Validate メソッドが例外をスローします)。Web API サービス メソッドで使用する図 1のコードは、ヘッダーからトークンを取得して検証します。

図 1 サービス メソッドにおける CSRF トークンの検証

public HttpResponseMessage Get(){
   if (Request.Headers.TryGetValues("cookieToken", out tokens))
   {
     string cookieToken = tokens.First();
     Request.Headers.TryGetValues("formToken", out tokens);
     string formToken = tokens.First();
     AntiForgery.Validate(cookieToken, formToken);
   }
   else
   {
     HttpResponseMessage hrm =
       new HttpResponseMessage(HttpStatusCode.Unauthorized);
     hrm.ReasonPhrase = "CSRF tokens not found";
     return hrm;
   } 
   // ... Code to process request ...

(メソッド内のコードではなく) ValidateHttpAntiForgeryToken を使用すると、処理のタイミングがサイクルの序盤 (モデル バインドの前など) になるというメリットがあります。

OAuth を使用しない理由

この記事では、あえて OAuth を無視しました。OAuth 仕様では、サードパーティのサーバーからクライアントにトークンを取得してサービスに送信し、その後このサービスで、サードパーティのサーバーを使用してトークンを検証する方法が定義されています。クライアントまたはサービスから OAuth トークン プロバイダーにアクセスする方法については、この記事では説明しません。

また、初期バージョンの OAuth と Web API の相性も良くありません。Web API を使用する主な理由の 1 つは、おそらく、REST と JSON に基づくより軽量な要求を使用することです。この目標のために、初期バージョンの OAuth は Web API サービスに関して魅力的な選択肢ではなくなっています。初期バージョンの OAuth で指定されているトークンは、サイズが大きく XML ベースです。さいわいなことに、OAuth 2.0 では、以前のバージョンのトークンよりもコンパクトな、比較的軽量な JSON トークンの仕様が導入されました。おそらく、この記事で説明している手法は、サービスに送信されたすべての OAuth トークンの処理に使用できます。

基本認証

Web API サービスのセキュリティ保護に関する、開発者の 2 つの主な責任の 1 つは、認証です (もう 1 つは承認です)。プライバシーのような他の課題については、ホストで対処するものとします。

認証と承認は、拒否する要求に処理サイクルを費やすことのないように、どちらも Web API パイプラインのできるだけ早い段階で実行することが理想的です。この記事の認証ソリューションは、パイプラインのかなり早い段階 (実質的には要求を受け取った直後) で使用します。また、これらの手法では、既に保持しているあらゆるユーザーの一覧に認証を統合することもできます。これから説明する承認手法は、パイプライン内のさまざまな時点 (サービス メソッド自体と同時になるほど遅い時点を含む) で利用でき、ユーザーの名前やロール以外の条件に基づいて要求を承認するために認証と連動できます。

カスタム HTTP モジュールで独自の認証方式を提供すると、フォーム認証を完了していないクライアントをサポートできます (ここでも、Windows アカウントに対してではなく、独自の有効なユーザーの一覧に対して認証しているものとします)。HTTP モジュールの使用には、モジュールが HTTP のログ記録と監査に関与し、パイプラインのとても早い段階で呼び出されるという 2 つの大きなメリットがあります。どちらも良いことではありますが、モジュールには 2 種類のコストがかかります。1 つはモジュールがグローバルなために Web API 要求だけでなくサイトに対するすべての要求に適用されることで、もう 1 つは認証モジュールを使用するには IIS のサービスをホストする必要があることです。この記事の後半では、Web API 要求でのみ呼び出され、ホストに依存しない委任ハンドラーの使用について説明します。

HTTP モジュールの使用に関するこの例では、IIS で基本認証を使用すると想定し、ユーザーの認証に使用する資格情報が、クライアントによって送信されたユーザー名とパスワードであると想定します (この記事では、Windows 証明の説明は省略しますが、クライアント証明書の使用については説明します)。また、保護対象の Web API サービスは、ユーザーを指定する次のコードのように、Authorize 属性を使用してセキュリティ保護されていると想定します。

public class CustomersController : ApiController
 {
   [Authorize(Users="Peter")]
   public Customer Get()
   {

カスタム承認 HTTP モジュールを作成する最初の手順は、IHttpModule インターフェイスと IDisposable インターフェイスを実装するサービス プロジェクトにクラスを追加することです。このクラスの Init メソッドでは、メソッドに渡される HttpApplication オブジェクトの 2 つのイベントを結び付ける必要があります。AuthenticateRequest イベントにアタッチするメソッドは、クライアントの資格情報が提示されると呼び出されます。ただし、クライアントから資格情報を送信させるメッセージを生成するために、EndRequest メソッドを設定する必要もあります。また、Dispose メソッドも必要ですが、コードを追加しなくても、以下で使用するコードをサポートできます。

public class PHVHttpAuthentication : IHttpModule, IDisposable
 {
   public void Init(HttpApplication context)
   {
     context.AuthenticateRequest += AuthenticateRequests;
     context.EndRequest += TriggerCredentials;
   }
   public void Dispose()
   {
   }

HTTP 応答に含める WWW-Authenticate ヘッダーへの応答として、HttpClient から資格情報が送信されます。要求によって 401 状態コードが生成される場合は、このヘッダーを含める必要があります (ASP.NET では、セキュリティ保護されたサービスへのクライアントによるアクセスが拒否されると、401 応答コードが生成されます)。ヘッダーでは、使用する認証方式と認証が適用される領域について、手掛かりを示す必要があります (この領域は、任意の文字列で指定可能で、ブラウザーに対してサーバー上のさまざまな領域のフラグを設定するために使用します)。このメッセージは、EndRequest イベントに関連付けたメソッドに含めているコードで送信します。次の例では、PHVIS 領域内で基本認証を使用するよう指定するメッセージを生成します。

private static void TriggerCredentials(object sender, EventArgs e)
 {
   HttpResponse resp = HttpContext.Current.Response;
   if (resp.StatusCode == 401)
   {
     resp.Headers.Add("WWW-Authenticate", @"Basic realm='PHVIS'");
   }
 }

AuthenticateRequests メソッドでは、401/WWW-Authenticate メッセージを受信した結果としてクライアントが送信する、Authorization ヘッダーを取得する必要があります。

private static void AuthenticateRequests(object sender,
   EventArgs e)
 {
   string authHeader =     
     HttpContext.Current. Request.Headers["Authorization"];
   if (authHeader != null)
   {

クライアントが Authorization ヘッダー要素を渡したことを確認したら (サイトで基本認証を使用するという先の前提は継続中です)、ユーザー名とパスワードが含まれているデータを解析する必要があります。ユーザー名とパスワードは、Base64 エンコードされており、コロンで区切られています。このコードは、ユーザー名とパスワードを 2 要素の文字列配列の形で取得します。

AuthenticationHeaderValue authHeaderVal =
   AuthenticationHeaderValue.Parse(authHeader);
 if (authHeaderVal.Parameter != null)
 {
   byte[] unencoded = Convert.FromBase64String(
     authHeaderVal.Parameter);
   string userpw =
     Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
   string[] creds = userpw.Split(':');

このコードが示すように、ユーザー名とパスワードはクリア テキストで送信されます。SSL を有効にしていない場合、ユーザー名とパスワードは簡単にキャプチャされるおそれがあります (このコードは SSL が有効でも機能します)。

次の手順では、任意の効果的なメカニズムを使用してユーザー名とパスワードを検証します。どの要求検証方法を採用する場合でも (次の例で使用するコードはおそらく単純すぎるでしょう)、最後の手順は、ASP.NET パイプラインの後半における承認プロセスで使用する、ユーザーの ID を作成することです。

パイプライン経由で ID 情報を渡すには、ユーザーに割り当てる ID の名前を使用して GenericIdentity オブジェクトを作成します (次のコードでは、この IDをヘッダーで送信されたユーザー名として想定しています)。GenericIdentity オブジェクトを作成したら、このオブジェクトを Thread クラスの CurrentPrincipal プロパティに格納する必要があります。また、ASP.NET では、2 つ目のセキュリティ コンテキストが HttpContext オブジェクトに保存されています。ホストが IIS の場合は、HttpContext の Current プロパティで User プロパティを GenericIdentity オブジェクトに設定して、このセキュリティ コンテキストをサポートする必要があります。

if (creds[0] == "Peter" && creds[1] == "pw")
 {
   GenericIdentity gi = new GenericIdentity(creds[0]);
   Thread.CurrentPrincipal = new GenericPrincipal(gi, null);
   HttpContext.Current.User = Thread.CurrentPrincipal;
 }

ロールベースのセキュリティをサポートする場合は、ロール名の配列を 2 つ目のパラメーターとして GenericPrincipal コンストラクターに渡す必要があります。次の例では、すべてのユーザーを manager ロールと admin ロールに割り当てます。

string[] roles = "manager,admin".Split(',');
 Thread.CurrentPrincipal = new GenericPrincipal(gi, roles);

HTTP モジュールをサイトの処理と統合するには、プロジェクトの web.config ファイルで、modules 要素の add タグを使用します。add タグの type 属性は、完全修飾クラス名の後にモジュールのアセンブリ名を追加した文字列に設定する必要があります。

<modules>
  <add name="myCustomerAuth"
    type="SecureWebAPI.PHVHttpAuthentication, SecureWebAPI"/>
</modules>

作成した GenericIdentity オブジェクトは ASP.NET の Authorize 属性と連動します。また、サービス メソッド内から GenericIdentity にアクセスして、承認処理を実行することもできます。たとえば、GenericIdentity オブジェクトの IsAuthenticated プロパティを確認して (IsAuthenticated は、匿名ユーザーに対して false を返します)、ユーザーが認証されているかどうかを識別すると、ログインしているユーザーと匿名ユーザーに異なるサービスを提供できます。

if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
 {

User プロパティを使用すると、GenericIdentity オブジェクトをさらに簡単に取得できます。

if (User.Identity.IsAuthenticated)
 {

互換性のあるクライアントを構築する

このモジュールによって保護されたサービスを使用するには、JavaScript 以外のクライアントから、受容可能なユーザー名とパスワードを渡す必要があります。.NET の HttpClient を使用してこのような資格情報を渡すには、まず HttpClientHandler オブジェクトを作成して、このオブジェクトの Credentials プロパティをユーザー名とパスワードが格納された NetworkCredential オブジェクトに設定します (または、HttpClientHandler オブジェクトの UseDefaultCredentials プロパティを true に設定し、現在のユーザーの Windows 資格情報を使用します)。次に、HttpClientHandler オブジェクトを渡して HttpClient オブジェクトを作成します。

HttpClientHandler hch = new HttpClientHandler();
 hch.Credentials = new NetworkCredential ("Peter", "pw");
 HttpClient hc = new HttpClient(hch);

この設定が完了すると、要求をサービスに発行できます。HttpClient は、サービスへのアクセスが拒否されて WWW-Authenticate メッセージを受け取るまで資格情報を提示しません。HttpClient から渡された資格情報が受容可能でない場合、サービスは、StatusCode の Result が "unauthenticated" に設定された HttpResponseMessage を返します。

次のコードでは、GetAsync メソッドを使用してサービスを呼び出し、成功した結果の有無を確認して、(成功した結果がない場合は) サービスから返された状態コードを表示します。

hc.GetAsync("http://phvis.com/api/Customers").ContinueWith(r =&gt;
 {
   HttpResponseMessage hrm = r.Result;
   if (hrm.IsSuccessStatusCode)
   {
     // ... Process response ...
   }
   else
   {
     MessageBox.Show(hrm.StatusCode.ToString());
   }
 });

このコードのように、JavaScript 以外のクライアントの ASP.NET ログイン処理を省略する場合、認証 Cookie は作成されず、クライアントからの各要求は個別に検証されます。クライアントから渡される資格情報を繰り返し検証することで生じるオーバーヘッドを減らすには、サービスで取得する資格情報のキャッシュ (およびキャッシュした資格情報を破棄する Dispose メソッドの使用) を検討してください。

クライアント証明書を操作する

HTTP モジュールでは、次のようなコードを使用してクライアント証明書オブジェクトを取得します (および、クライアント証明書オブジェクトが存在していて有効なことを確認します)。

System.Web.HttpClientCertificate cert =
   HttpContext.Current.Request.ClientCertificate;
 if (cert!= null && cert.IsPresent && cert.IsValid)
 {

処理パイプラインのさらに先の段階 (サービス メソッドなど) では、次のコードを使用して証明書オブジェクトを取得します (また、その証明書オブジェクトが存在していることを確認します)。

X509Certificate2 cert = Request.GetClientCertificate();
 if (cert!= null)
 {

証明書が有効で存在している場合は、証明書のプロパティに設定された特定の値 (サブジェクトや発行者など) も追加で確認できます。

HttpClient を使用して証明書を送信するには、まず、HttpClientHandler の代わりに WebRequestHandler オブジェクトを作成します (WebRequestHandler には、HttpClientHandler よりも多くの構成オプションが用意されています)。

WebRequestHandler wrh = new WebRequestHandler();

WebRequestHandler オブジェクトの ClientCertificateOptions を ClientCertificateOption 列挙体の Automatic に設定すると、HttpClient でクライアントの証明書ストアを自動検索できます。

wrh.ClientCertificateOptions = ClientCertificateOption.Manual;

ただし、既定では、クライアントで証明書をコードの WebRequestHandler に明示的にアタッチする必要があります。次の例のように、証明書はクライアントのいずれかの証明書ストアから取得できます (この例では、発行者の名前を使用して、CurrentUser のストアから証明書を取得しています)。

X509Store certStore;
 X509Certificate x509cert;
 certStore = new X509Store(StoreName.My, 
   StoreLocation.CurrentUser);  
 certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
 x509cert = certStore.Certificates.Find(
   X509FindType.FindByIssuerName, "PHVIS", true)[0];
 store.Close();

なんらかの理由でユーザーの証明書ストアに追加されないクライアント証明書がユーザーに送信された場合は、次のようなコードを使用して、証明書のファイルから X509Certificate オブジェクトを作成します。

x509cert = new X509Certificate2(@"C:\PHVIS.pfx");

X509Certificate のどの作成方法を採用する、クライアントにおける最後の手順は、証明書を WebRequestHandler の ClientCertificates コレクションに追加し、構成した WebRequestHandler を使用して HttpClient を作成することです。

wrh.ClientCertificates.Add(x509cert);
 hc = new HttpClient(wrh);

自己ホスト環境で承認する

自己ホスト環境では HttpModule を使用できませんが、自己ホスト型サービスの処理パイプラインの初期に要求をセキュリティ保護する処理は同じです。要求から資格情報を取得し、その情報を使用して要求を認証し、現在のスレッドの CurrentPrincipal プロパティに渡す ID を作成します。最も単純なメカニズムは、ユーザー名とパスワードの検証コントロールを作成することです。ユーザー名とパスワードの組み合わせの検証よりも高度な処理を実行するには、委任ハンドラーを作成できます。まずは、ユーザー名とパスワードの検証コントロールの統合について見ていきましょう。

検証コントロールを作成するには (ここでも基本認証を使用しているものとします)、UserNamePasswordValidator から継承するクラスを作成する必要があります (System.IdentityModel ライブラリへの参照をプロジェクトに追加する必要があります)。基本クラスからオーバーライドする必要があるメソッドは、Validate メソッドだけです。Validate メソッドには、クライアントからサービスに送信されたユーザー名とパスワードを渡します。先ほどと同様に、ユーザー名とパスワードを検証したら、GenericPrincipal オブジェクトを作成し、このオブジェクトを使用して Thread クラスの CurrentPrincipal プロパティを設定する必要があります (ホストとして IIS を使用していないため、HttpContext の User プロパティは設定しません)。

public class PHVValidator :
   System.IdentityModel.Selectors.UserNamePasswordValidator
 {
   public override void Validate(string userName, string password)
   {
     if (userName == "Peter" && password == "pw")
     {
       GenericIdentity gi = new GenericIdentity(username, null);
       Thread.CurrentPrincipal = gi;
     }

次のコードは、エンドポイントの http://phvis.com/MyServices を使用して、Customers というコントローラー用にホストを作成し、新しい検証コントロールを指定します。

partial class PHVService : ServiceBase
 {
   private HttpSelfHostServer shs;
   protected override void OnStart(string[] args)
   {
     HttpSelfHostConfiguration hcfg =
       new HttpSelfHostConfiguration("http://phvis.com/MyServices");
     hcfg.Routes.MapHttpRoute("CustomerServiceRoute",
       "Customers", new { controller = "Customers" });
     hcfg.UserNamePasswordValidator = new PHVValidator;       
     shs = new HttpSelfHostServer(hcfg);
     shs.OpenAsync();

メッセージ ハンドラー

ユーザー名とパスワードの検証よりも高度な処理を実行するには、カスタム Web API メッセージ ハンドラーを作成できます。メッセージ ハンドラーには、HTTP モジュールと比べていくつかのメリットがあります。たとえば、メッセージ ハンドラーは IIS に依存しないので、メッセージ ハンドラーに適用されたセキュリティは、あらゆるホストで使用できます。メッセージ ハンドラーを使用するのは Web API のみなので、Web サイト ページで使用する処理とは異なる処理を使用して、簡単にサービスの承認 (および ID の割り当て) を行うことができます。また、セキュリティ コードが必要なときにだけ呼び出されるように、特定のルートにメッセージ ハンドラーを割り当てることもできます。

メッセージ ハンドラーを作成する最初の手順では、DelegatingHandler から継承するクラスを記述して、このクラスの SendAsync メソッドをオーバーライドします。

public class PHVAuthorizingMessageHandler: DelegatingHandler
 {
   protected override System.Threading.Tasks.Task
     SendAsync(HttpRequestMessage request,
       System.Threading.CancellationToken cancellationToken)
   {

このメソッド内部では (ルートごとにハンドラーを作成していると想定します)、他のハンドラーを使用するパイプラインにこのハンドラーをリンクできるよう、DelegatingHandler の InnerHandler プロパティを設定できます。

HttpConfiguration hcon = request.GetConfiguration();
 InnerHandler = new HttpControllerDispatcher(hcon);

この例では、有効な要求ではクエリ文字列に単純なトークン ("authToken=xyx" の名前/値のペアというきわめて単純なもの) が含まれている必要があると想定します。トークンがない場合、またはトークンが xyx に設定されていない場合、コードは 403 (Forbidden) 状態コードを返します。

まずは、メソッドに渡された HttpRequestMessage オブジェクトの GetQueryNameValuePairs メソッドを呼び出すことで、クエリ文字列を名前/値のペアに変換します。次に、LINQ を使用してトークンを取得します (トークンがない場合は null を取得します)。トークンがないか無効な場合は、適切な HTTP 状態コードを使用して HttpResponseMessage を作成し、TaskCompletionSource オブジェクトでラップして返します。

string usingRegion = (from kvp in request.GetQueryNameValuePairs()
                       where kvp.Key == "authToken"
                       select kvp.Value).FirstOrDefault();
 if (usingRegion == null || usingRegion != "xyx")
 {
   HttpResponseMessage resp =
      new HttpResponseMessage(HttpStatusCode.Forbidden);
   TaskCompletionSource tsc =
      new TaskCompletionSource();
   tsc.SetResult(resp);
   return tsc.Task;
 }

トークンが存在していて適切な値に設定されている場合は、GenericPrincipal オブジェクトを作成し、このオブジェクトを使用して Thread の CurrentPrincipal プロパティを設定します (IIS でこのメッセージ ハンドラーの使用をサポートするために、HttpContext オブジェクトが null でない場合は HttpContext の User プロパティも設定します)。

Thread.CurrentPrincipal = new GenericPrincipal(
   Thread.CurrentPrincipal.Identity.Name, null);     
 if (HttpContext.Current != null)
 {
   HttpContext.Current.User = Thread.CurrentPrincipal;
 }

要求がトークンを介して認証され、ID が設定されたら、メッセージ ハンドラーで基本メソッドを呼び出して処理を続行します。

return base.SendAsync(request, cancellationToken);

メッセージ ハンドラーをすべてのコントローラーで使用する場合は、他のメッセージ ハンドラーと同様に、そのメッセージ ハンドラーを Web API 処理パイプラインに追加できます。しかし、ハンドラーを特定のルートだけで使用するよう制限するには、ハンドラーの追加に MapHttpRoute メソッドを使用する必要があります。まず、クラスのインスタンスを作成し、そのクラスを 5 番目のパラメーターとして MapHttpRoute に渡します (このコードには、System.Web.Http の Imports/using ステートメントが必要です)。

routes.MapHttpRoute(
   "ServiceDefault",
   "api/Customers/{id}",
   new { id = RouteParameter.Optional },
   null,
   new PHVAuthorizingMessageHandler());

InnerHandler プロパティを DelegatingHandler 内で設定せずに、ルートを定義する一環としてこのプロパティを既定のディスパッチャーに設定できます。

routes.MapHttpRoute(
   "ServiceDefault",
   "api/{controller}/{id}",
   new { id = RouteParameter.Optional },
   null,
   new PHVAuthorizingMessageHandler
   {InnerHandler = new HttpControllerDispatcher(
     GlobalConfiguration.Configuration)});

これで、InnerHandler 設定が、複数の DelegatingHandler に分散するのではなく、ルートを定義した 1 つの場所から管理されるようになりました。

プリンシパルを拡張する

名前とロールによる要求の承認が不十分な場合は、IPrincipal インターフェイスを実装して独自のプリンシパル クラスを作成することで、承認プロセスを拡張できます。しかし、カスタム プリンシパル クラスを利用するには、独自のカスタム承認属性を作成するか、サービス メソッドにカスタム コードを追加する必要があります。

たとえば、特定地域のユーザーのみがアクセスできる一連のサービスがある場合は、IPrincipal インターフェイスを実装し Region プロパティを追加する、単純なプリンシパル クラスを作成できます (図 2参照)。

図 2 追加のプロパティを使用したカスタム プリンシパルの作成

public class PHVPrincipal: IPrincipal
 {
   public PHVPrincipal(string Name, string Region)
   {
     this.Name = Name;
     this.Region = Region;
   }
   public string Name { get; set; }
   public string Region { get; set; }
   public IIdentity Identity
   {
     get
     {
       return new GenericIdentity(this.Name);
     }
     set
     {
       this.Name = value.Name;
     }
    }
    public bool IsInRole(string role)
    {
      return true;
    }

この (あらゆるホストで機能する) 新しいプリンシパル クラスを利用するには、クラスのインスタンスを作成し、そのインスタンスを使用して CurrentPrincipal プロパティと User プロパティを設定するだけで十分です。次のコードでは、要求の、"region" という名前に関連付けられたクエリ文字列内にある値を探します。値を取得したら、その値をクラスのコンストラクターに渡して、プリンシパルの Region プロパティの設定に使用します。

string region = (from kvp in request.GetQueryNameValuePairs()
                  where kvp.Key == "region"
                  select kvp.Value).FirstOrDefault();
 Thread.CurrentPrincipal = new PHVPrincipal(userName, region);

Microsoft .NET Framework 4.5 で作業している場合は、IPrincipal インターフェイスを実装するのではなく、新しい ClaimsPrincipal クラスから継承してください。ClaimsPrincipal では、クレームベースの処理も、Windows Identity Foundation (WIF) との統合もサポートしています。ただし、これについてはこの記事では説明しません (このトピックについては、クレームベースのセキュリティに関する今後の記事で取り上げます)。

カスタム プリンシパルを承認する

新しいプリンシパル オブジェクトを準備できたら、プリンシパルによって渡された新しいデータを利用する承認属性を作成できます。まず、System.Web.Http.AuthorizeAttribute から継承するクラスを作成し、そのクラスの IsAuthorized メソッドをオーバーライドします (これは、System.Web.Http.Filters.AuthorizationFilterAttribute を拡張して新しい Authorization 属性を作成する ASP.NET MVC の手法とは異なる処理です)。IsAuthorized メソッドには、承認プロセスの一環でプロパティを使用できる HttpActionContext を渡します。しかし、この例で必要な作業は、Thread の CurrentPrincipal プロパティからプリンシパル オブジェクトを抽出し、このオブジェクトをカスタム プリンシパルの型にキャストして、Region プロパティを確認することだけです。承認が成功した場合はコードで true を返し、失敗した場合は、actionContext の Response プロパティでカスタム応答を作成してから false を返す必要があります (図 3参照)。

図 3 カスタム プリンシパル オブジェクトのフィルター処理

public class RegionAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
 {
   public string Region { get; set; }
   protected override bool IsAuthorized(HttpActionContext actionContext)
   {
     PHVPrincipal phvPcp = Thread.CurrentPrincipal as PHVPrincipal;
     if (phvPcp != null && phvPcp.Region == this.Region)
     {
       return true;
     }
     else
     {
       actionContext.Response =
         new HttpResponseMessage(
           System.Net.HttpStatusCode.Unauthorized)
         {
           ReasonPhrase = "Invalid region"
         };
       return false;
     }        
   }
 }

カスタム承認フィルターは、ASP.NET の既定の Authorize フィルターと同じように使用できます。この例のフィルターには Region プロパティがあるので、サービス メソッドを Region プロパティで修飾する際に、Region プロパティの値をこのメソッドの受容可能な地域に設定する必要があります。

[RegionAuthorize(Region = "East")]
 public HttpResponseMessage Get()
 {

この例では、承認コードが純粋に CPU の制約を受けるので、AuthorizeAttribute から継承することにしました。ネットワーク リソースにアクセスする (または、少しでも I/O を実行する) ためにこのコードが必要だとしたら、IAuthorizaionFilter インターフェイスを実装する方が適しているでしょう。その理由は、IAuthorizaionFilter インターフェイスが非同期呼び出しをサポートしているからです。

この記事の冒頭で述べたように、通常の Web API に関するシナリオでは、CSFR 攻撃の防止を除けば追加の承認は必要ありません。しかし、既定のセキュリティ システムを拡張する必要がある場合は、Web API には処理パイプライン全体にわたって多数の方法が用意されているので、必要なあらゆる保護を組み込むことができます。どのようなときも選択肢はあった方が良いものです。

Peter Vogel は、サービス指向アーキテクチャ、XML、データベース、および UI デザインの専門技術を持ち ASP.NET 開発に特化した企業、PH&V Information Services の社長です。

この記事のレビューに協力してくれた技術スタッフの Dominick Baier (thinktecture GmbH & Co KG)、Barry Dorrans (マイクロソフト)、および Mike Wasson (マイクロソフト) に心より感謝いたします。
Mike Wasson (mwasson@microsoft.com、英語のみ) は、マイクロソフトのプログラマ兼ライターです。現在は、Web API に焦点を絞った ASP.NET に関する記事を執筆しています。

Barry Dorrans (Barry.Dorrans@microsoft.com、英語のみ) は、マイクロソフトのセキュリティ デベロッパーであり、Azure Platform チームと連携して活動しています。また、『Beginning ASP.NET Security』の著者であり、マイクロソフトに入社する前は Developer Security MVP でもありました。それにもかかわらず、彼は今でも日常的に "暗号化" (encryption) のスペルを間違えています。

Dominick (dominick.baier@thinktecture.com、英語のみ) は、thinktecture 社 (thinktecture.com、英語) のセキュリティ コンサルタントです。主な専門分野は分散アプリケーションにおける ID とアクセスの管理です。また、人気のオープン ソース プロジェクトである IdentityModel と IdentityServer の作者でもあります。彼のブログは leastprivilege.com(英語) で公開されています。