webdevqa.jp.net

カスタムWeb API HttpMessageHandlerでユーザープリンシパルを安全に設定するにはどうすればよいですか?

基本認証については、ここにあるDarin Dimitrovの回答に示されている例に基づいて、カスタムHttpMessageHandlerを実装しました。 https://stackoverflow.com/a/11536349/270591

このコードは、ユーザー名とロールを持つタイプprincipalのインスタンスGenericPrincipalを作成し、このプリンシパルをスレッドの現在のプリンシパルに設定します。

Thread.CurrentPrincipal = principal;

後でApiControllerメソッドで、コントローラーUserプロパティにアクセスすることでプリンシパルを読み取ることができます。

public class ValuesController : ApiController
{
    public void Post(TestModel model)
    {
        var user = User; // this should be the principal set in the handler
        //...
    }
}

最近MediaTypeFormatterライブラリを使用するカスタムTaskを追加するまで、これはうまくいくように見えました。

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream,
    HttpContent content, IFormatterLogger formatterLogger)
{
    var task = Task.Factory.StartNew(() =>
    {
        // some formatting happens and finally a TestModel is returned,
        // simulated here by just an empty model
        return (object)new TestModel();
    });
    return task;
}

(いくつかのサンプルコードからReadFromStreamAsyncTask.Factory.StartNewでタスクを開始するこのアプローチがあります。それは間違っているのでしょうか、それが問題の唯一の理由ですか?)

さて、「時々」-そして私にとってはランダムに見える-コントローラーメソッドのUserプリンシパルは、MessageHandlerで設定したプリンシパル、つまりユーザー名、Authenticatedフラグではなくなりましたそして役割はすべて失われます。理由は、カスタムMediaTypeFormatterがMessageHandlerとコントローラーメソッドの間でスレッドの変更を引き起こすためと思われます。 MessageHandlerとコントローラーメソッドでThread.CurrentThread.ManagedThreadIdの値を比較することでこれを確認しました。 「時々」それらは異なり、そしてプリンシパルは「失われます」。

Thread.CurrentPrincipalを設定して、カスタムMessageHandlerからコントローラーメソッドにプリンシパルを安全に転送し、 このブログ投稿 リクエストプロパティを使用する代わりの方法を探しました。

request.Properties.Add(HttpPropertyKeys.UserPrincipalKey,
    new GenericPrincipal(identity, new string[0]));

私はそれをテストしたかったのですが、HttpPropertyKeysクラス(名前空間System.Web.Http.Hostingにある)には、最近のWebApiバージョン(リリース候補と最終リリース)でUserPrincipalKeyプロパティがもうないようです先週からも)。

私の質問は、上記の最後のコードスニペットを現在のWebAPIバージョンで動作するように変更するにはどうすればよいですか?または一般的に:カスタムMessageHandlerでユーザープリンシパルを設定し、コントローラーメソッドで確実にアクセスするにはどうすればよいですか?

編集

here に言及されている「HttpPropertyKeys.UserPrincipalKey ...は“MS_UserPrincipal”」に解決されるので、私は使用しようとしました:

request.Properties.Add("MS_UserPrincipal",
    new GenericPrincipal(identity, new string[0]));

しかし、期待どおりに機能しません。ApiController.Userプロパティには、上記のPropertiesコレクションに追加されたプリンシパルが含まれていません。

50
Slauma

新しいスレッドでプリンシパルを失う問題は、次のとおりです。

http://leastprivilege.com/2012/06/25/important-setting-the-client-principal-in-asp-net-web-api/

重要:ASP.NET Web APIでクライアントプリンシパルを設定する

ASP.NETに深く埋め込まれたいくつかの不幸なメカニズムのため、Web API WebホスティングでThread.CurrentPrincipalを設定するだけでは十分ではありません。

ASP.NETでホストしている場合、新しいスレッドの作成時にThread.CurrentPrincipalがHttpContext.Current.Userでオーバーライドされる場合があります。つまり、スレッドとHTTPコンテキストの両方でプリンシパルを設定する必要があります。

そしてここ: http://aspnetwebstack.codeplex.com/workitem/264

今日、カスタムメッセージハンドラーを使用してWebホストシナリオで認証を実行する場合、ユーザープリンシパルに次の両方を設定する必要があります。

IPrincipal principal = new GenericPrincipal(
    new GenericIdentity("myuser"), new string[] { "myrole" });
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;

最後の行を追加しましたHttpContext.Current.User = principal(必要なusing System.Web;)メッセージハンドラへ、およびUser内のApiControllerプロパティは、MediaTypeFormatterのタスクによりスレッドが変更された場合でも、常に正しいプリンシパルを持つようになりました。

編集

強調するために、現在のユーザーのHttpContextのプリンシパルの設定は、WebApiがASP.NET/IISでホストされている場合にのみ必要です。セルフホスティングの場合、それは必要ありません(HttpContextはASP.NET構造であり、セルフホスティングでは存在しないため不可能です)。

75
Slauma

カスタムMessageHandlerを使用して、MS_UserPrincipalで定義されているHttpRequestMessageExtensionMethods.SetUserPrincipal拡張メソッドを呼び出すことにより、System.ServiceModel.Channelsプロパティを追加できます。

protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    var user = new GenericPrincipal(new GenericIdentity("UserID"), null);
    request.SetUserPrincipal(user);
    return base.SendAsync(request, cancellationToken);
}

これは、このプロパティをリクエストのプロパティコレクションに追加するだけであり、ApiControllerにアタッチされたユーザーは変更しないことに注意してください。

5
gmoody1979

コンテキストの切り替えを回避するには、TaskCompletionSource<object>カスタムMediaTypeFormatterで別のタスクを手動で開始する代わりに:

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
    var tcs = new TaskCompletionSource<object>();

    // some formatting happens and finally a TestModel is returned,
    // simulated here by just an empty model
    var testModel = new TestModel();

    tcs.SetResult(testModel);
    return tcs.Task;
}
5
Darin Dimitrov