許可されていないwebapi呼び出しが401ではなくログインページを返す


180

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

そのMVC5アプリケーションには、JavaScriptを介した呼び出し用のWebApiコントローラーもあります。

以下の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.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番目のメソッドは、(success関数で)ログインページに相当するデータを返します。つまり、[Authorize]でスタンプされたコントローラアクションをリクエストし、ログインしていなかった場合にmvcで取得できるもの。

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

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

回答:


78

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パイプラインが引き継ぎ、いくつかの新しい動作を導入すると思います。これで、ユーザーが認証されない場合、200のステータスが返され、HTTPヘッダーに次の情報が含まれます。

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

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

私は、カスタムでこの動作を無効にしようとしたAuthorizeAttributeで応答して、ステータスを設定することにより、OnAuthorizationHandleUnauthorizedRequest方法。

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

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

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

X-Requested-With: XMLHttpRequest

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


私はSystem.web.http.authorizeattributeを使用していると思いますが、少なくともこのwebapicontrollerにはsystem.web.mvcの使用がありません。authorize属性の定義に行くと、system.web.httpに送信されます
Tim

こんにちは@ kevin-junghansはここで完全に混乱しています。上記のshivaの例では、確かにwebapiアクションに適用するべきではないmvc承認属性を使用しています。Brockallenの例は、動作しているように見えません。
Tim

1
この答えを見つけただけです(stackoverflowが通知を送信しないと考えてください)。問題を説明するためにgithubの例を追加し、角度のヘッダーに修正を追加しました。ありがとう。しかし、authorize属性に確認できるプロパティがないか、あなたが言及した元の機能が機能しなくなったように思われます。
Tim

3
POSTMANとヘッダーパラメータX-Requested-Withの使用:XMLHttpRequestは私にとっては
うまくいき

それで、これを行う純粋なWeb APIプロジェクトになるつもりがある場合はどうでしょうか。私は他の誰かがセットアップしたプロジェクトに取り組んでいて、Authorizeはここで説明するようにリダイレクトしていますが、正常に機能する別のAPIプロジェクトがあります。これをAPIアプリではなくMVCアプリだと思わせるものがあるに違いありませんが、何がそれを悪用しているのかわかりません。
Derek Greer

123

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"));
}

68
これのバリエーション:すべてのWeb API呼び出しが特定のパス(例:)を経由する/api場合、そのパスを使用してリダイレクトするかどうかを決定できます。JSONなどの他の形式を使用するクライアントがある場合、これは特に便利です。呼び出しに置き換えIsAjaxRequestとしますif (!context.Request.Path.StartsWithSegments(new PathString("/api")))
Edward Brey

パーティーには遅れますが、この方法は私にとって有効な唯一の方法であり、より「正確」なようです。
スティーブンコリンズ

パーティーに遅れても(r)、これは非常に便利であることが証明されています...生成されたデフォルトのコードがこれほど間違って、いらいらさせるほど難しいデバッグ方法で実行するという私の心を揺さぶります。
ニック

WebApiソリューションを求めている場合、Manikの回答は、ここで高く投票されたコメントの良い代替案です。
2015年

5
C#6を使用して、ここでIsAjaxRequestの小さいバージョンがある: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
ピーターÖrneholm

85

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);
}

1
判別式を変更して、リクエストがtext / htmlまたはapplication / xhtmlをレスポンスとして受け入れるかどうかを確認し、それが「自動化された」クライアントリクエストであると想定しない場合は、そのようなAjaxリクエスト
L.Trabacchin

4
私もこのアプローチを好みます。私が行った唯一の追加は、「/ API」または何かを要求する場合に備えてLocalPath .ToLower()を変換することでした。
FirstDivision 2016年

1
どうもありがとう。それは私の日を救った。:)
Amit Kumar

誰かがこれで運がいいですか?aspnet core 1.1以降、CookieAuthenticationOptionsにはProviderプロパティがなくなりました。
ジェレミー

27

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

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

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;
}

ありがとうございました !Xamarin / Androidを除くすべてのプレートフォームでAPIが機能しました。このソリューションを使用します
ジュリオン

17

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;
}

編集この記事ご覧くださいがやろうとしていることを達成するためのより洗練された方法を持つ、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);
    }
}

2番目–条件を追加しましょう::ユーザーが認証された場合、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のすべての使用をこの新しいフィルターに置き換える必要があります。コードの趣味が悪いsimeの人には適用できないかもしれません。しかし、私は他の方法を知りません。もしあれば、コメントに行きましょう。

最後に、何をすべきか–クライアント側に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呼び出しの後にログインページにリダイレクトされます。
  • ユーザーが認証されているが、十分な権限がない場合、ユーザーフレンドリーなerorrメッセージが表示されます。
  • ユーザーが認証され、十分な権限がある場合、エラーは発生せず、HTTPリクエストは通常​​どおり続行されます。

私はmvc経由の認証に新しいIDフレームワークを使用しています。この設定は、mvcログインとwebapi呼び出しの動作を妨げませんか?
Tim

5
この例を確認したところ、使用されている認証属性がWebApiバージョンではなくMVCバージョンであるようです。ただし、webapiバージョンには、フォーム認証を抑制するオプションがありません。
Tim

私のリクエストにはIsAjaxRequestメソッドがありません。
Tim

1
IsAjaxRequestため、この時のティムを見て:brockallen.com/2013/10/27/... あなたが「XMLHttpRequestを」を持っており、どちらかそれを追加したり、何か他のものをチェックしませんヘッダを編集せずAngularJsを使用している場合。
Tim

10

プロジェクトWeb API内からを実行している場合は、メソッドに適用MVCするカスタムAuthorizeAttributeを作成する必要がありますAPI。以下のように、リダイレクトを防ぐためにIsAuthorized override電流を取得する必要があります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);
    }

8

私自身、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を処理して、アプリにアクセスできなくなり、再ログインして再度ログインする必要があることをユーザーに通知します。


これは、関連する質問のために提案されたソリューションと非常によく似ています。stackoverflow.com
Guillaume LaHaye

6

また、WebOpi(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の動作を変更しないようにしました。


作品が、今私は、クライアントがステータスを受信することを-1の代わりに401の問題を抱えている
セバスティアン・ロハス

@SebastiánRojas何が原因かはSuppressFormsAuthenticationRedirect わかりません。フラグを設定すると、既存の401が返されました。
Jono Job 2016

3

次の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"));
    }
}

私がする必要があるのは、このフィルターを追加することだけで、config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));それ以外の場合User.Identity.IsAuthenticatedは常に機能しますfalse
Ricardo Saracino

1

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

OnAuthorization / HandleUnauthorizedRequestメソッドでステータスコードとテキスト応答の両方を取得するのに苦労しました。これは私にとって最善の解決策であることがわかりました:

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

1

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

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

1

MVCとWebAPIを混在させると、リクエストが無許可の場合、WebAPIリクエストでもログインページにリダイレクトされます。そのために、以下のコードを追加して、モバイルアプリケーションに応答を送信できます。

protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
    var httpContext = HttpContext.Current;
    if (httpContext == null)
    {
        base.HandleUnauthorizedRequest(actionContext);
        return;
    }

    actionContext.Response = httpContext.User.Identity.IsAuthenticated == false ?
        actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Unauthorized, "Unauthorized") :
       actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Forbidden, "Forbidden");

    httpContext.Response.SuppressFormsAuthenticationRedirect = true;
    httpContext.Response.End();
}

0

みんなありがとう!

私の場合、私は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);
                }
            }
        }
    });

-1

Dot Net Framework 4.5.2のMVC 5では、「Accept」ヘッダーの下に「application / json、plaint text ..」が表示されます。次のように使用すると便利です。

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.