web-dev-qa-db-ja.com

異常な要件に合わせてASP.NET Web API AuthorizeAttributeをカスタマイズする方法

System.Web.Http.AuthorizeAttributeを継承して、ASP.NET MVC 4を使用して開発されたWebアプリケーションの異常な要件を満たすカスタムの認証/認証ルーチンを作成します。これにより、Web APIにセキュリティが追加されます。 WebクライアントからのAjax呼び出しに使用されます。要件は次のとおりです。

  1. ユーザーは、トランザクションを実行するたびにログオンして、誰かがログオンして立ち去った後、他の誰かがワークステーションに立ち上がっていないことを確認する必要があります。
  2. プログラム時にロールをWebサービスメソッドに割り当てることはできません。管理者がこれを構成できるように、実行時に割り当てる必要があります。この情報はシステムデータベースに保存されます。

Webクライアントは シングルページアプリケーション(SPA) であるため、一般的なフォーム認証はそれほどうまく機能しませんが、要件を満たすためにできるだけ多くのASP.NETセキュリティフレームワークを再利用しようとしています。 。カスタマイズされたAuthorizeAttributeは、Webサービスメソッドに関連付けられているロールを決定する要件2に最適です。メソッドに関連付けられているロールを決定するために、アプリケーション名、リソース名、および操作の3つのパラメーターを受け入れます。

public class DoThisController : ApiController
{
    [Authorize(Application = "MyApp", Resource = "DoThis", Operation = "read")]
    public string GetData()
    {
        return "We did this.";
    }
}

OnAuthorizationメソッドをオーバーライドして、ロールを取得し、ユーザーを認証します。ユーザーはトランザクションごとに認証される必要があるため、同じステップで認証と承認を実行することで、やり取りを減らすことができます。 HTTPヘッダーで暗号化された資格情報を渡す基本認証を使用して、Webクライアントからユーザー資格情報を取得します。したがって、私のOnAuthorizationメソッドは次のようになります。

public override void OnAuthorization(HttpActionContext actionContext)
{

     string username;
     string password;
     if (GetUserNameAndPassword(actionContext, out username, out password))
     {
         if (Membership.ValidateUser(username, password))
         {
             FormsAuthentication.SetAuthCookie(username, false);
             base.Roles = GetResourceOperationRoles();
         }
         else
         {
             FormsAuthentication.SignOut();
             base.Roles = "";
         }
     }
     else
     {
         FormsAuthentication.SignOut();
         base.Roles = "";
     }
     base.OnAuthorization(actionContext);
 }

GetUserNameAndPasswordは、HTTPヘッダーから資格情報を取得します。次に、Membership.ValidateUserを使用して、資格情報を検証します。カスタムデータベースにアクセスするためにプラグインされたカスタムメンバーシッププロバイダーとロールプロバイダーがあります。ユーザーが認証された場合、リソースと操作のロールを取得します。そこから、ベースOnAuthorizationを使用して認証プロセスを完了します。ここで分解します。

ユーザーが認証された場合、標準のフォーム認証方法を使用してユーザーをログインし(FormsAuthentication.SetAuthCookie)、失敗した場合はログアウトします(FormsAuthentication.SignOut)。しかし、問題はベースOnAuthorizationクラスがIsAuthenticatedが正しい値に設定されるように更新されるPrincipalにアクセスできないということのようです。常に一歩遅れています。そして、私の推測では、Webクライアントへの往復があるまで更新されないキャッシュされた値を使用していると考えられます。

したがって、これらすべてが私の特定の質問につながります。つまり、Cookieを使用せずにIsAuthenticatedを現在のPrincipalの正しい値に設定する別の方法がありますか?毎回認証する必要があるこの特定のシナリオでは、Cookieは実際には適用されないようです。 IsAuthenticatedが正しい値に設定されていないことを知っている理由は、HandleUnauthorizedRequestメソッドもこれにオーバーライドしているためです。

 protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
 {
     if (((System.Web.HttpContext.Current.User).Identity).IsAuthenticated)
     {
         filterContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
     }
     else
     {
         base.HandleUnauthorizedRequest(filterContext);
     }
 }

これにより、失敗が認証ではなく承認が原因であり、それに応じて応答できる場合、ForbiddenのステータスコードをWebクライアントに返すことができます。

このシナリオで現在のPrincipleIsAuthenticatedを設定する適切な方法は何ですか?

35
Kevin Junghans

私のシナリオに最適なソリューションは、ベースOnAuthorizationを完全にバイパスすることです。毎回Cookieとキャッシュを認証する必要があるため、原則はあまり役に立ちません。だからここに私が思いついた解決策があります:

public override void OnAuthorization(HttpActionContext actionContext)
{
    string username;
    string password;

    if (GetUserNameAndPassword(actionContext, out username, out password))
    {
        if (Membership.ValidateUser(username, password))
        {
            if (!isUserAuthorized(username))
                actionContext.Response = 
                    new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            actionContext.Response = 
                new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        }
    }
    else
    {
        actionContext.Response = 
            new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
    }
}

isUserAuthorizedと呼ばれるロールを検証するための独自のメソッドを開発し、現在のPrincipleを確認するため、ベースOnAuthorizationを使用していませんそれがあればisAuthenticatedIsAuthenticatedはgetのみを許可するため、他にどのように設定すればよいかわかりません。現在のPrincipleは必要ないようです。これをテストし、正常に動作します。

誰かがより良い解決策を持っているか、これに関する問題を見ることができるなら、まだ興味があります。

38
Kevin Junghans

すでに受け入れられている回答に追加するには、現在のソースコード(aspnetwebstack.codeplex.com)でSystem.Web.Http.AuthorizeAttributeを確認すると、ドキュメントが古くなっているように見えます。ベースOnAuthorization()は、プライベートスタティックSkipAuthorization()を呼び出し/チェックするだけです(コンテキストでAllowAnonymousAttributeが残りの認証チェックをバイパスするために使用されるかどうかをチェックするだけです)。次に、スキップされない場合、OnAuthorization()はpublic IsAuthorized()を呼び出し、その呼び出しが失敗した場合、保護された仮想HandleUnauthorizedRequest()を呼び出します。そして、それだけです...

public override void OnAuthorization(HttpActionContext actionContext)
{
    if (actionContext == null)
    {
        throw Error.ArgumentNull("actionContext");
    }

    if (SkipAuthorization(actionContext))
    {
        return;
    }

    if (!IsAuthorized(actionContext))
    {
        HandleUnauthorizedRequest(actionContext);
    }
}

IsAuthorized()の中を見ると、ここでPrincipleがロールとユーザーに対してチェックされます。したがって、IsAuthorized()の代わりにOnAuthorization()を上にあるものでオーバーライドする方法があります。繰り返しますが、OnAuthorization()またはHandleUnauthorizedRequest()のいずれかをオーバーライドして、401レスポンスと403レスポンスのどちらを返すかを決定する必要があります。

25

Kevinの絶対的な正解に追加するために、フレームワーク(または他のコンシューマー)のダウンストリームコードが悪影響を受けないように、応答オブジェクトの既存の.NETフレームワークパスを活用するように少し変更することができます。予測できないいくつかの奇妙な特異性によって。

具体的には、次のコードを使用することを意味します。

actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, REQUEST_NOT_AUTHORIZED);

のではなく:

actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);

REQUEST_NOT_AUTHORIZEDは次のとおりです。

private const string REQUEST_NOT_AUTHORIZED = "Authorization has been denied for this request.";

.NETフレームワークのSRResources.RequestNotAuthorized定義からstringを引き出しました。

すばらしい答えケビン!基本クラスでOnAuthorizationを実行しても意味がなかったので、アプリケーションにカスタムのHTTPヘッダーを検証し、実際にはプリンシパルをチェックしたくないため、同じ方法で実装しました。じゃない.

4
Mike Perrenoud