web-dev-qa-db-ger.com

Nicht autorisierter Web-API-Aufruf, der die Anmeldeseite statt 401 zurückgibt

Wie konfiguriere ich mein mvc/webapi-Projekt so, dass eine aus einer Rasiereransicht aufgerufene webapi-Methode die Anmeldeseite nicht zurückgibt, wenn sie nicht autorisiert ist?

Es ist eine MVC5-Anwendung, die auch WebApi-Controller für Aufrufe über Javascript enthält.

Die zwei folgenden Methoden

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

werden über folgenden Winkelcode aufgerufen:

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

Ich bin also nicht angemeldet und die erste Methode gibt erfolgreich Daten zurück. Die zweite Methode gibt (in der Erfolgsfunktion) Daten zurück, die das Äquivalent einer Anmeldeseite enthalten. d. h., was Sie in mvc erhalten würden, wenn Sie eine Controller-Aktion anfordern, die mit [Authorize] versehen wurde und Sie nicht angemeldet sind.

Ich möchte, dass eine nicht autorisierte 401 zurückgegeben wird, sodass verschiedene Daten für Benutzer angezeigt werden können, je nachdem, ob sie angemeldet sind oder nicht. Wenn der Benutzer angemeldet ist, möchte ich im Idealfall auf die Benutzereigenschaft des Controllers zugreifen können, um Daten zu diesem Mitglied zurückzugeben.

UPDATE: Da keiner der folgenden Vorschläge mehr zu funktionieren scheint (Änderungen an Identity oder WebAPI), erstellte ive auf github ein Rohbeispiel, das das Problem veranschaulichen sollte.

164
Tim

Es gibt zwei AuthorizeAttribute-Implementierungen, und Sie müssen sicherstellen, dass Sie auf die richtige Version für Web-APIs verweisen. Es gibt System.Web.Http.AuthorizeAttribute , das für Web-APIs verwendet wird, und System.Web.Mvc.AuthorizeAttribute , das für Controller verwendet wird Ansichten. Http.AuthorizeAttribute gibt einen Fehler 401 zurück, wenn die Autorisierung fehlschlägt, und Mvc.AuthorizeAttribute wird zur Anmeldeseite umgeleitet.

Aktualisiert am 26.11.2013

Es scheint, als hätten sich die Dinge mit MVC 5 drastisch verändert, als Brock Allen auf in seinem Artikel hinwies. Ich denke, die OWIN-Pipeline übernimmt und führt ein neues Verhalten ein. Wenn der Benutzer nicht berechtigt ist, wird jetzt ein Status von 200 mit den folgenden Informationen im HTTP-Header zurückgegeben.

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

Sie können Ihre Logik auf der Clientseite ändern, um diese Informationen im Header zu überprüfen, um zu ermitteln, wie sie damit umgehen sollen, anstatt im Fehlerzweig nach einem 401-Status zu suchen. 

Ich habe versucht, dieses Verhalten in einem benutzerdefinierten AuthorizeAttribute zu überschreiben, indem ich den Status in der Antwort in den Methoden OnAuthorization und HandleUnauthorizedRequest festlegte.

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

Das hat aber nicht funktioniert. Die neue Pipeline muss diese Antwort später greifen und auf dieselbe Antwort umstellen, die ich zuvor erhalten habe. Das Auslösen einer HttpException funktionierte ebenfalls nicht, da sie nur in einen Fehlerstatus von 500 geändert wurde.

Ich habe die Lösung von Brock Allen getestet und es funktionierte, als ich einen jQuery-Ajax-Anruf verwendete. Wenn es für Sie nicht funktioniert, schätze ich, dass Sie Winkel verwenden. Führen Sie Ihren Test mit Fiddler aus und prüfen Sie, ob die folgenden Angaben in Ihrem Header enthalten sind.

X-Requested-With: XMLHttpRequest

Wenn nicht, dann ist das das Problem. Ich bin nicht mit eckigen Eigenschaften vertraut, aber wenn Sie damit Ihre eigenen Header-Werte einfügen können, fügen Sie dies Ihren Ajax-Anforderungen hinzu, und es wird wahrscheinlich funktionieren.

77
Kevin Junghans

Brock Allen hat einen schönen Blogbeitrag dazu, wie Sie 401 für Ajax-Anrufe zurückgeben können, wenn Sie die Cookie-Authentifizierung und OWIN . http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with verwenden -web-api-und-401-antwortcodes/

Fügen Sie dies in die ConfigureAuth-Methode in der Datei Startup.Auth.cs ein:

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"));
}
109
Olav Nybø

Wenn Sie asp.net WebApi auf der asp.net MVC-Website hinzufügen, möchten Sie möglicherweise unberechtigt auf einige Anfragen antworten. Aber dann kommt die ASP.NET-Infrastruktur ins Spiel und wenn Sie versuchen, den Antwortstatuscode auf HttpStatusCode zu setzen. Wenn Sie nicht autorisiert sind, erhalten Sie eine Weiterleitung zur Anmeldeseite.

Wenn Sie asp.net identity und owin-basierte Authentifizierung verwenden, finden Sie hier einen Code, der zur Behebung dieses Problems beitragen kann:

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);
}
77
Manik Arora

Ich habe die gleiche Situation, wenn OWIN 401 immer die Antwort auf die Anmeldeseite von WebApi umleitet. Unsere Web-API unterstützt nicht nur Ajax-Aufrufe von Angular, sondern auch Mobile, Win Form-Aufrufe. Daher ist die Lösung zur Überprüfung, ob es sich bei der Anfrage um eine Ajax-Anfrage handelt, für unseren Fall nicht wirklich sortiert.

Ich habe mich für einen anderen Ansatz entschieden, eine neue Header-Antwort einzufügen: Suppress-Redirect, wenn die Antworten von webApi stammen. Die Implementierung erfolgt auf Handler:

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

Registrieren Sie diesen Handler auf der globalen Ebene von WebApi:

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

Beim Start von OWIN können Sie also prüfen, ob der Antwortheader Suppress-Redirect hat:

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

In früheren Versionen von ASP.NET mussten Sie eine ganze Reihe von Dingen erledigen , damit dies funktioniert.

Die gute Nachricht ist, dass Sie ASP.NET 4.5 verwenden. Sie können die Formularauthentifizierungsumleitung mit der neuen Eigenschaft HttpResponse.SuppressFormsAuthenticationRedirect deaktivieren.

In Global.asax:

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

EDIT: Vielleicht möchten Sie auch einen Blick auf in diesem Artikel von Sergey Zwezdin werfen, der eine verfeinerte Art und Weise hat Erreichen, was Sie versuchen zu tun.

Relevante Code-Schnipsel und Autoren-Kommentare werden unten eingefügt. Originalautor von Code und Erzählung - Sergey Zwezdin .

Zuerst stellen wir fest, ob es sich bei der aktuellen HTTP-Anfrage um eine AJAX-Anfrage handelt. Wenn ja, sollten wir das Ersetzen von HTTP 401 durch HTTP 302 deaktivieren:

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

Zweitens: Fügen wir eine Bedingung hinzu: Wenn der Benutzer authentifiziert ist, senden wir HTTP 403. und HTTP 401 ansonsten.

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

Gut gemacht. Jetzt sollten wir alle Verwendungen von AuthorizeAttribute durch diesen neuen Filter ersetzen. Dies gilt möglicherweise nicht für Sime-Typen, die sich mit Code auskennen. Aber ich kenne keinen anderen Weg. Wenn ja, gehen wir bitte zu den Kommentaren.

Das letzte, was wir tun sollten - um eine clientseitige HTTP 401/403-Behandlung hinzuzufügen. Wir können ajaxError bei jQuery verwenden, um Codeduplizierungen zu vermeiden:

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

Das Ergebnis -

  • Wenn der Benutzer nicht authentifiziert ist, wird er nach jedem AJAX-Aufruf auf eine Anmeldeseite umgeleitet.
  • Wenn der Benutzer authentifiziert ist, aber nicht über ausreichende Berechtigungen verfügt, wird eine benutzerfreundliche Fehlermeldung angezeigt.
  • Wenn der Benutzer authentifiziert ist und über ausreichende Berechtigungen verfügt, gibt es keine Fehler und die HTTP-Anforderung wird wie gewohnt ausgeführt.
15
Shiva

Durch die Azure Active Directory-Integration selbst hat der Ansatz mit der CookieAuthentication-Middleware für mich nicht funktioniert. Ich musste folgendes tun:

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

Wenn die Anfrage vom Browser selbst kommt (und nicht etwa aus einem Aufruf von AJAX), enthält der Accept-Header irgendwo den String html. Nur wenn der Client HTML akzeptiert, halte ich eine Weiterleitung für nützlich. 

Meine Client-Anwendung kann mit dem 401 umgehen und den Benutzer darüber informieren, dass die App keinen Zugriff mehr hat und zum erneuten Anmelden erneut geladen werden muss.

8

Wenn Sie Ihren Web API in Ihrem MVC-Projekt ausführen, müssen Sie eine benutzerdefinierte AuthorizeAttribute erstellen, um sie auf Ihre API-Methoden anzuwenden. Innerhalb der Variablen IsAuthorizedoverride müssen Sie die aktuelle Variable HttpContext ergreifen, um die Umleitung zu verhindern, wie folgt:

    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
Serj Sagan

Ich hatte auch eine MVC5-Anwendung (System.Web) mit WebApi (mit OWIN) und wollte nur verhindern, dass 401 Antworten von WebApi zu 302 Antworten geändert werden.

Was für mich funktionierte, war die Erstellung einer benutzerdefinierten Version des WebApi AuthorizeAttribute wie folgt:

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

Und um es anstelle des Standard-WebApi AuthorizeAttribute zu verwenden. Ich habe das Standard-MVC-AuthorizeAttribute verwendet, um das MVC-Verhalten unverändert zu lassen.

3
Jono Job

wenn Sie Content-Type == application/json abrufen möchten, können Sie diesen Code verwenden: 

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;

    }

grüße!!

1
chemitaxis

Nach langen Versuchen, die Weiterleitungen zur Anmeldeseite zu vermeiden, wurde mir klar, dass dies für das Authorize-Attribut durchaus angemessen ist. Es heißt, gehen Sie und erhalten Sie die Genehmigung. Anstatt für Api-Aufrufe, die nicht autorisiert sind, wollte ich einfach keine Informationen preisgeben, die Hacker wären .. Dieses Ziel war einfacher zu erreichen, indem ein neues Attribut hinzugefügt wurde, das von Authorize abgeleitet wurde, und der Inhalt stattdessen als 404-Fehler ausgeblendet wird:

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

Ich hatte Schwierigkeiten, sowohl den Statuscode als auch eine Textantwort in den OnAuthorization/HandleUnauthorizedRequest-Methoden zu erhalten. Dies stellte sich als die beste Lösung für mich heraus:

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

Installieren Sie einfach das folgende NeGet-Paket

Installationspaket Microsoft.AspNet.WebApi.Owin

Schreiben Sie folgenden Code in die WebApiConfig-Datei.

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

Danke Leute!

In meinem Fall kombinierte ich die Antworten von cuongle & Shiva und erhielt so etwas:

Im Controller-Handler OnException () für API-Ausnahmen:

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

Im App-Start-Konfigurationscode:

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