web-dev-qa-db-fra.com

Page de connexion renvoyant un appel d'API Web non autorisé plutôt que 401

Comment configurer mon projet mvc/webapi de sorte qu'une méthode webapi appelée à partir d'une vue rasoir ne renvoie pas la page de connexion lorsqu'elle est non autorisée?

C'est une application MVC5 qui possède également des contrôleurs WebApi pour les appels via javascript.

Les deux méthodes ci-dessous

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

sont appelés via le code angulaire suivant:

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

Je ne suis donc pas connecté et la première méthode renvoie les données avec succès. la seconde méthode renvoie (dans la fonction success) des données contenant l'équivalent d'une page de connexion. c’est-à-dire ce que vous obtiendrez dans mvc si vous demandiez une action du contrôleur portant l’indication [Autoriser] et que vous n’étiez pas connecté.

Je veux qu'il retourne un 401 non autorisé, afin que je puisse afficher différentes données pour les utilisateurs en fonction de leur connexion ou non. Idéalement, si l'utilisateur est connecté, je souhaite pouvoir accéder à la propriété Utilisateur du contrôleur afin de pouvoir renvoyer des données spécifiques à ce membre.

UPDATE: Comme aucune des suggestions ci-dessous ne semble plus fonctionner (modifications apportées à Identity ou WebAPI), ive a créé un exemple brut sur github qui devrait illustrer le problème.

164
Tim

Il existe deux implémentations AuthorizeAttribute et vous devez vous assurer de référencer celle qui convient pour les API Web. Il y a System.Web.Http.AuthorizeAttribute utilisé pour les API Web et System.Web.Mvc.AuthorizeAttribute utilisé pour les contrôleurs avec vues. (Http.AuthorizeAttribute} retournera une erreur 401 si l'autorisation échoue et Mvc.AuthorizeAttribute redirigera vers la page de connexion.

Mis à jour le 26/11/2013

Il semble donc que les choses ont radicalement changé avec MVC 5, comme l'a souligné Brock Allen dans son article . Je suppose que le pipeline OWIN prend le relais et introduit un nouveau comportement. Désormais, lorsque l'utilisateur n'est pas autorisé, un statut de 200 est renvoyé avec les informations suivantes dans l'en-tête HTTP.

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

Vous pouvez modifier votre logique côté client pour vérifier ces informations dans l'en-tête et déterminer comment le gérer, au lieu de rechercher un statut 401 dans la branche d'erreur. 

J'ai essayé de remplacer ce comportement dans les méthodes AuthorizeAttribute personnalisées en définissant le statut dans la réponse dans les méthodes OnAuthorization et HandleUnauthorizedRequest.

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

Mais cela n'a pas fonctionné. Le nouveau pipeline doit saisir cette réponse plus tard et la modifier pour qu'elle corresponde à celle que j'avais auparavant. Le lancement d'une exception HttpException ne fonctionnait pas non plus car il est simplement remplacé par un statut d'erreur 500.

J'ai testé la solution de Brock Allen et cela a fonctionné lorsque j'utilisais un appel jQuery ajax. Si cela ne fonctionne pas pour vous, je suppose que c'est parce que vous utilisez angular. Exécutez votre test avec Fiddler et voyez si ce qui suit est dans votre en-tête.

X-Requested-With: XMLHttpRequest

Si ce n'est pas le cas, c'est le problème. Je ne connais pas bien angular, mais s'il vous permet d'insérer vos propres valeurs d'en-tête, ajoutez-le à vos requêtes ajax et cela commencera probablement à fonctionner.

77
Kevin Junghans

Brock Allen a publié un article sur le blog de Nice sur la façon de renvoyer 401 pour les appels ajax lors de l'utilisation de l'authentification par cookie et de OWIN . http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with -web-api-and-401-response-codes/

Placez ceci dans la méthode ConfigureAuth dans le fichier Startup.Auth.cs:

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ø

Si vous ajoutez asp.net WebApi sur le site Web asp.net MVC, vous souhaiterez probablement répondre de manière non autorisée à certaines demandes. Mais alors l'infrastructure ASP.NET entre en jeu et lorsque vous essayez de définir le code d'état de la réponse sur HttpStatusCode.Unauthorized, vous obtiendrez une redirection 302 vers la page de connexion.

Si vous utilisez l’identité asp.net et l’authentification basée sur owin, voici un code qui peut vous aider à résoudre ce problème:

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

J'ai eu la même situation lorsque OWIN redirige toujours la réponse 401 vers la page de connexion de WebApi.Notre API Web prend en charge non seulement les appels ajax à partir d'appels Angular, mais également les appels Mobile, Win Form. Par conséquent, la solution pour vérifier si la requête est une requête ajax n'est pas vraiment triée pour notre cas.

J'ai opté pour une autre approche consiste à injecter une nouvelle réponse d'en-tête: Suppress-Redirect si les réponses proviennent de webApi. L'implémentation est sur 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);
    }
}

Et enregistrez ce gestionnaire au niveau global de WebApi:

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

Ainsi, au démarrage d'OWIN, vous êtes en mesure de vérifier si l'en-tête de la réponse a 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;
}
23
cuongle

Dans les versions précédentes d'ASP.NET, il fallait faire tout un tas de choses pour que cela fonctionne.

La bonne nouvelle est que vous utilisez ASP.NET 4.5. vous pouvez désactiver la redirection d'authentification par formulaire à l'aide de la nouvelle propriété HttpResponse.SuppressFormsAuthenticationRedirect .

Dans Global.asax:

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

EDIT: Vous voudrez peut-être aussi jeter un oeil à à cet article de Sergey Zwezdin qui a une manière plus raffinée de accomplir ce que vous essayez de faire.

Extraits de code pertinents et narration d'auteur collés ci-dessous. Auteur d'origine du code et de la narration - Sergey Zwezdin .

Premièrement - déterminons si la requête HTTP actuelle est une requête AJAX. Si oui, nous devrions désactiver le remplacement de HTTP 401 par 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);
    }
}

Deuxièmement - ajoutons une condition :: si l’utilisateur est authentifié, nous enverrons HTTP 403; et HTTP 401 sinon.

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

Bien joué. Nous devons maintenant remplacer toutes les utilisations de AuthorizeAttribute standard par ce nouveau filtre. Cela peut ne pas être applicable pour les gars de Sime, qui est esthète du code. Mais je ne connais aucun autre moyen. Si vous avez, passons aux commentaires, s'il vous plaît.

La dernière chose à faire est d’ajouter la gestion HTTP 401/403 côté client. Nous pouvons utiliser ajaxError sur jQuery pour éviter la duplication de code:

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

Le résultat -

  • Si l'utilisateur n'est pas authentifié, il sera redirigé vers une page de connexion après tout appel AJAX.
  • Si l'utilisateur est authentifié mais ne dispose pas d'autorisations suffisantes, il verra le message erorr convivial.
  • Si l'utilisateur est authentifié et dispose d'autorisations suffisantes, il n'y a aucune erreur et la requête HTTP sera traitée comme d'habitude.
15
Shiva

En utilisant moi-même l'intégration d'Azure Active Directory, l'approche utilisant le middleware CookieAuthentication ne fonctionnait pas pour moi. Je devais faire ce qui suit:

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

Si la demande provient du navigateur lui-même (et non d'un appel AJAX, par exemple), l'en-tête Accept contient la chaîne html quelque part. Ce n'est que lorsque le client accepte le langage HTML que je considère une redirection comme quelque chose d'utile. 

Mon application client peut gérer les informations 401 informant l'utilisateur que l'application n'a plus d'accès et doit être rechargée pour se reconnecter.

8
Dave Van den Eynde

Si vous exécutez votre Web API à partir de votre projet MVC, vous devez créer une AuthorizeAttribute personnalisée à appliquer à vos méthodes API. Dans IsAuthorizedoverride, vous devez récupérer la HttpContext actuelle afin d'empêcher la redirection, comme ceci:

    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

J'avais aussi une application MVC5 (System.Web) avec WebApi (avec OWIN) et je voulais simplement empêcher que 401 réponses de WebApi ne soient modifiées en 302 réponses.

Ce qui a fonctionné pour moi a été de créer une version personnalisée de WebApi AuthorizeAttribute comme ceci:

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

Et de l’utiliser à la place de la norme WebApi AuthorizeAttribute. J'ai utilisé le standard MVC AuthorizeAttribute pour conserver le comportement de MVC inchangé.

3
Jono Job

si vous voulez intercepter Content-Type == application/json, vous pouvez utiliser ce code: 

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;

    }

cordialement!!

1
chemitaxis

Après avoir tenté d'éviter les redirections vers la page de connexion, je me suis rendu compte que c'était en fait tout à fait approprié pour l'attribut Authorize. Il est dit aller chercher une autorisation. Au lieu de cela, pour les appels Api non autorisés, je voulais simplement ne révéler aucune information aux hackers . Cet objectif était plus facile à atteindre directement en ajoutant un nouvel attribut dérivé de Authorize qui cache le contenu sous la forme d'une erreur 404:

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

J'avais du mal à obtenir le code de statut et une réponse textuelle dans les méthodes OnAuthorization/HandleUnauthorizedRequest. Cela s'est avéré être la meilleure solution pour moi:

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

Il suffit d'installer le paquet suivant NeGet

Package d'installation Microsoft.AspNet.WebApi.Owin

Écrivez le code suivant dans le fichier 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

Merci les gars!

Dans mon cas, j'ai combiné les réponses de cuongle & Shiva , et j'ai obtenu quelque chose comme ceci:

Dans le gestionnaire OnException () du contrôleur pour les exceptions d'API:

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

Dans le code de configuration de démarrage de l'application:

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