webdevqa.jp.net

401ではなくログインページを返す不正なWeb API呼び出し

かみそりビューから呼び出されたwebapiメソッドが許可されていないときにログインページを返さないように、mvc/webapiプロジェクトを構成するにはどうすればよいですか?

JavaScriptを介した呼び出し用のWebApiコントローラーも備えたMVC5アプリケーション。

以下の2つの方法

[Route("api/home/LatestProblems")]      
[HttpGet()]
public List<vmLatestProblems> LatestProblems()
{
    // Something here
}

[Route("api/home/myLatestProblems")]
[HttpGet()]
[Authorize(Roles = "Member")]
public List<vmLatestProblems> mylatestproblems()
{
   // Something there
}

次のangularコードを介して呼び出されます。

angular.module('appWorship').controller('latest', 
    ['$scope', '$http', function ($scope,$http) {         
        var urlBase = baseurl + '/api/home/LatestProblems';
        $http.get(urlBase).success(function (data) {
            $scope.data = data;
        }).error(function (data) {
            console.log(data);
        });
        $http.get(baseurl + '/api/home/mylatestproblems')
          .success(function (data) {
            $scope.data2 = data;
        }).error(function (data) {
            console.log(data);
        });  
    }]
);

だから私はログインしておらず、最初のメソッドはデータを正常に返します。 2番目のメソッドは、ログインページに相当するものを含むデータを(成功関数で)返します。つまり、[Authorize]でスタンプされたコントローラーアクションを要求し、ログインしていない場合にmvcで取得するものです。

私は、ユーザーがログインしているかどうかに基づいてユーザーにさまざまなデータを表示できるように、401不正を返します。理想的には、ユーザーがログインしている場合、そのメンバーに固有のデータを返すことができるように、コントローラーのユーザープロパティにアクセスできるようにします。

更新:以下の提案はいずれも機能しないようであるため(IdentityまたはWebAPIの変更)、問題を示すために github の未加工の例を作成しました。

171
Tim

AuthorizeAttributeの実装は2つあり、Web APIの正しい実装を参照していることを確認する必要があります。 System.Web.Http.AuthorizeAttribute はWeb APIに使用され、 System.Web.Mvc .AuthorizeAttribute これは、ビューを持つコントローラーに使用されます。 Http.AuthorizeAttributeは、認証に失敗すると401エラーを返し、Mvc.AuthorizeAttributeはログインページにリダイレクトします。

2013年11月26日更新

そのため、ブロックアレンが の記事 で指摘したように、MVC 5では状況が劇的に変化したようです。 OWINパイプラインが新しい動作を引き継いで導入すると思います。ユーザーが認証されていない場合、HTTPヘッダーに次の情報を含むステータス200が返されます。

X-Responded-JSON: {"status":401,"headers":{"location":"http:\/\/localhost:59540\/Account\/Login?ReturnUrl=%2Fapi%2FTestBasic"}}

エラーブランチで401ステータスを探す代わりに、クライアント側でロジックを変更してヘッダーのこの情報を確認し、これを処理する方法を決定できます。

OnAuthorizationおよびHandleUnauthorizedRequestメソッドで応答のステータスを設定することにより、カスタムAuthorizeAttributeでこの動作をオーバーライドしようとしました。

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

しかし、これはうまくいきませんでした。新しいパイプラインは、後でこの応答を取得し、前に取得した同じ応答に変更する必要があります。 HttpExceptionのスローは、500エラーステータスに変更されただけなので、機能しませんでした。

Brock Allenのソリューションをテストしましたが、jQuery ajax呼び出しを使用していたときに機能しました。それがあなたのために働いていない場合、私の推測はあなたが角度を使用しているためだと思います。 Fiddlerでテストを実行し、ヘッダーに以下が含まれているかどうかを確認します。

X-Requested-With: XMLHttpRequest

そうでない場合、それが問題です。私はangularに慣れていませんが、独自のヘッダー値を挿入できる場合は、これをajaxリクエストに追加すると、おそらく動作し始めます。

79
Kevin Junghans

Brock Allenには、Cookie認証とOWINを使用している場合にajax呼び出しに対して401を返す方法に関する素晴らしいブログ投稿があります。 http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

これをStartup.Auth.csファイルのConfigureAuthメソッドに追加します。

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
  AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  LoginPath = new PathString("/Account/Login"),
  Provider = new CookieAuthenticationProvider
  {
    OnApplyRedirect = ctx =>
    {
      if (!IsAjaxRequest(ctx.Request))
      {
        ctx.Response.Redirect(ctx.RedirectUri);
      }
    }
  }
});

private static bool IsAjaxRequest(IOwinRequest request)
{
  IReadableStringCollection query = request.Query;
  if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
  {
     return true;
  }
  IHeaderDictionary headers = request.Headers;
  return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest"));
}
113
Olav Nybø

Asp.net MVC Webサイト内にasp.net WebApiを追加している場合、おそらくいくつかのリクエストに不正に応答したいでしょう。ただし、ASP.NETインフラストラクチャが機能し、応答ステータスコードをHttpStatusCode.Unauthorizedに設定しようとすると、ログインページへの302リダイレクトが発生します。

ここでasp.net IDとowinベースの認証を使用している場合、その問題を解決するのに役立つコード:

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                if (!IsApiRequest(ctx.Request))
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}


private static bool IsApiRequest(IOwinRequest request)
{
    string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
    return request.Uri.LocalPath.StartsWith(apiPath);
}
80
Manik Arora

OWINが常に401A応答をWebApiからログインページにリダイレクトするときも同じ状況になりました。当社のWeb APIは、Angularからのajax呼び出しだけでなく、モバイル、Win Form呼び出しもサポートします。したがって、リクエストがajaxリクエストであるかどうかを確認するソリューションは、このケースでは実際にはソートされていません。

別のアプローチとして、新しいヘッダーレスポンスを挿入することを選択しました。webApiからのレスポンスの場合はSuppress-Redirectです。実装はハンドラー上にあります。

public class SuppressRedirectHandler : DelegatingHandler
{
    /// <summary>
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(task =>
        {
            var response = task.Result;
            response.Headers.Add("Suppress-Redirect", "True");
            return response;
        }, cancellationToken);
    }
}

そして、このハンドラーをWebApiのグローバルレベルに登録します。

config.MessageHandlers.Add(new SuppressRedirectHandler());

そのため、OWINの起動時に、応答ヘッダーにSuppress-Redirectが含まれているかどうかを確認できます。

public void Configuration(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = DefaultApplicationTypes.ApplicationCookie,
        ExpireTimeSpan = TimeSpan.FromMinutes(48),

        LoginPath = new PathString("/NewAccount/LogOn"),

        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                var response = ctx.Response;
                if (!IsApiResponse(ctx.Response))
                {
                    response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
}

private static bool IsApiResponse(IOwinResponse response)
{
    var responseHeader = response.Headers;

    if (responseHeader == null) 
        return false;

    if (!responseHeader.ContainsKey("Suppress-Redirect"))
        return false;

    if (!bool.TryParse(responseHeader["Suppress-Redirect"], out bool suppressRedirect))
        return false;

    return suppressRedirect;
}
24
cuongle

ASP.NETの以前のバージョンでは、これを機能させるために 大量の処理を行う が必要でした。

良いニュースは、ASP.NET 4.5を使用していることです。新しい HttpResponse.SuppressFormsAuthenticationRedirect プロパティを使用して、フォーム認証リダイレクトを無効にできます。

Global.asaxで:

protected void Application_EndRequest(Object sender, EventArgs e)
{
        HttpApplication context = (HttpApplication)sender;
        context.Response.SuppressFormsAuthenticationRedirect = true;
}

EDIT:ご覧になることもできます この記事で Sergey Zwezdinにより、あなたがやろうとしていることを達成する。

関連するコードスニペットと著者のナレーションを以下に貼り付けます。コードとナレーションの元の著者- Sergey Zwezdin

まず、現在のHTTPリクエストがAJAXリクエストであるかどうかを判断しましょう。はいの場合、HTTP 401をHTTP 302に置き換えることを無効にする必要があります。

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;

        if (request.IsAjaxRequest())
            response.SuppressFormsAuthenticationRedirect = true;

        base.HandleUnauthorizedRequest(filterContext);
    }
}

次に、条件を追加します。ユーザーが認証された場合、HTTP 403を送信します。それ以外の場合はHTTP 401。

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;
        var user = httpContext.User;

        if (request.IsAjaxRequest())
        {
            if (user.Identity.IsAuthenticated == false)
                response.StatusCode = (int)HttpStatusCode.Unauthorized;
            else
                response.StatusCode = (int)HttpStatusCode.Forbidden;

            response.SuppressFormsAuthenticationRedirect = true;
            response.End();
        }

        base.HandleUnauthorizedRequest(filterContext);
    }
}

よくやった。ここで、標準のAuthorizeAttributeのすべての使用をこの新しいフィルターに置き換える必要があります。それはコードの美人である悪人には適用できないかもしれません。しかし、私は他の方法を知りません。もしあれば、コメントに行きましょう。

最後に、クライアント側でHTTP 401/403処理を追加する必要があります。 jQueryでajaxErrorを使用して、コードの重複を回避できます。

$(document).ajaxError(function (e, xhr) {
    if (xhr.status == 401)
        window.location = "/Account/Login";
    else if (xhr.status == 403)
        alert("You have no enough permissions to request this resource.");
});

結果 -

  • ユーザーが認証されていない場合、AJAX呼び出しの後にログインページにリダイレクトされます。
  • ユーザーが認証されているが、十分な権限がない場合、ユーザーにわかりやすいエラーメッセージが表示されます。
  • ユーザーが認証され、十分な権限がある場合、エラーは発生せず、HTTPリクエストは通常​​どおり処理されます。
15
Shiva

MVCプロジェクト内からWeb APIを実行している場合、AuthorizeAttributeメソッドに適用するカスタムAPIを作成する必要があります。 IsAuthorizedoverride内では、次のように、リダイレクトを防ぐために現在のHttpContextを取得する必要があります。

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        if (string.IsNullOrWhiteSpace(Thread.CurrentPrincipal.Identity.Name))
        {
            var response = HttpContext.Current.Response;
            response.SuppressFormsAuthenticationRedirect = true;
            response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
            response.End();
        }

        return base.IsAuthorized(actionContext);
    }
9
Serj Sagan

Azure Active Directory統合を自分で使用した場合、CookieAuthenticationミドルウェアを使用したアプローチはうまくいきませんでした。私は次のことをしなければなりませんでした:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...
        Notifications = new OpenIdConnectAuthenticationNotifications
        {   
            ...         
            RedirectToIdentityProvider = async context =>
            {
                if (!context.Request.Accept.Contains("html"))
                {
                    context.HandleResponse();
                }
            },
            ...
        }
    });

リクエストがブラウザ自体から来た場合(たとえば、AJAX呼び出しではない場合)、Acceptヘッダーのどこかに文字列htmlが含まれます。クライアントがHTMLを受け入れる場合にのみ、リダイレクトが有用であると考えます。

私のクライアントアプリケーションは、アプリにこれ以上アクセスできず、再度ログインするためにリロードする必要があることをユーザーに通知する401を処理できます。

8

また、WebApi(OWINを使用)を備えたMVC5アプリケーション(System.Web)があり、WebApiからの401応答が302応答に変更されないようにしたかっただけです。

私のために働いたのは、次のようなWebApi AuthorizeAttributeのカスタマイズされたバージョンを作成することでした:

public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);
        HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

そして、標準のWebApi AuthorizeAttributeの代わりに使用します。標準のMVC AuthorizeAttributeを使用して、MVCの動作を変更しませんでした。

3
Jono Job

content-Type == application/jsonをキャッチしたい場合、そのコードを使用できます。

private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;

    }

よろしく!!

1
chemitaxis

ログインページへのリダイレクトを回避しようとして大騒ぎした後、これが実際にAuthorize属性に非常に適していることに気付きました。行って認可を取得するということです。代わりに、承認されていないApi呼び出しについては、ハッカーとなる情報を開示しないようにしたかっただけです。この目的は、Authorizeから派生した新しい属性を追加することで直接達成するのが簡単でした。代わりに、404エラーとしてコンテンツを非表示にします。

public class HideFromAnonymousUsersAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
         actionContext.Response = ActionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Restricted");
    }
}
1
user3879365

OnAuthorization/HandleUnauthorizedRequestメソッドでステータスコードとテキストレスポンスの両方を取得するのに苦労していました。これは私にとって最良のソリューションであることが判明しました。

    actionContext.Response = new HttpResponseMessage()
    {
        StatusCode = HttpStatusCode.Forbidden,
        Content = new StringContent(unauthorizedMessage)
    };
1
PutoTropical

次のNeGetパッケージをインストールするだけです

インストールパッケージMicrosoft.AspNet.WebApi.Owin

WebApiConfigファイルに次のコードを記述します。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Web API configuration and services
        //Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }
}
1
user8477754

みんなありがとう!

私の場合、 cuongleShiva の回答を組み合わせて、次のようなものを得ました:

API例外用のコントローラーのOnException()ハンドラー内:

filterContext.ExceptionHandled = true;
//...
var response = filterContext.HttpContext.Response;
response.Headers.Add("Suppress-Redirect", "true");
response.SuppressFormsAuthenticationRedirect = true;

アプリのスタートアップ構成コード:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            OnValidateIdentity = ctx => {
                return validateFn.Invoke(ctx);
            },
            OnApplyRedirect = ctx =>
            {
                bool enableRedir = true;
                if (ctx.Response != null)
                {
                    string respType = ctx.Response.ContentType;
                    string suppress = ctx.Response.Headers["Suppress-Redirect"];
                    if (respType != null)
                    {
                        Regex rx = new Regex("^application\\/json(;(.*))?$",
                            RegexOptions.IgnoreCase);
                        if (rx.IsMatch(respType))
                        {
                            enableRedir = false;
                        }  
                    }
                    if ((!String.IsNullOrEmpty(suppress)) && (Boolean.Parse(suppress)))
                    {
                        enableRedir = false;
                    }
                }
                if (enableRedir)
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
0
QuaOs