web-dev-qa-db-fra.com

ASP.NET_SessionId + OWIN Les cookies ne sont pas envoyés au navigateur.

J'ai un problème étrange d'utilisation de l'authentification par cookie Owin.

Lorsque je lance mon IIS), l'authentification du serveur fonctionne parfaitement sous IE/Firefox et Chrome.

J'ai commencé à faire des tests avec l'authentification et la connexion sur différentes plates-formes et j'ai rencontré une erreur étrange. Sporadiquement, le framework Owin/IIS n'envoie aucun cookie aux navigateurs. Je vais taper un nom d'utilisateur et un mot de passe corrects pour que le code soit exécuté, mais aucun cookie n'est livré au navigateur. Si je redémarre le serveur, il commence à fonctionner, puis, à un moment donné, j'essaie de me connecter et les cookies cessent d'être livrés.

 app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Active,
            CookieHttpOnly = true,
            AuthenticationType = "ABC",
            LoginPath = new PathString("/Account/Login"),
            CookiePath = "/",
            CookieName = "ABC",
            Provider = new CookieAuthenticationProvider
               {
                  OnApplyRedirect = ctx =>
                  {
                     if (!IsAjaxRequest(ctx.Request))
                     {
                        ctx.Response.Redirect(ctx.RedirectUri);
                     }
                 }
               }
        });

Et dans ma procédure de connexion, j'ai le code suivant:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
                            authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

var authentication = HttpContext.Current.GetOwinContext().Authentication;
var identity = new ClaimsIdentity("ABC");
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.User_ID.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Role, role.myRole.ToString()));
    authentication.AuthenticationResponseGrant =
        new AuthenticationResponseGrant(identity, new AuthenticationProperties()
                                                   {
                                                       IsPersistent = isPersistent
                                                   });

authenticationManager.SignIn(new AuthenticationProperties() {IsPersistent = isPersistent}, identity);

Mise à jour 1: Il semble qu'une des causes du problème soit lorsque j'ajoute des éléments à une session, les problèmes commencent. Ajouter quelque chose de simple comme Session.Content["ABC"]= 123 Semble créer le problème.

Voici ce que je peux comprendre: 1) (Chrome) Lorsque je me connecte, je reçois ASP.NET_SessionId + mon cookie d'authentification. 2) Je vais sur une page qui définit un contenu de session ... 3) Ouvrez un nouveau navigateur (Firefox) et essayez de vous connecter. Il ne reçoit ni ASP.NET_SessionId ni un cookie d'authentification 4) Alors que le premier navigateur a l'ASP.NET_SessionId, il continue à fonctionner. La minute où je supprime ce cookie, le même problème que tous les autres navigateurs sur lesquels je travaille est l'adresse IP (10.x.x.x) et l'hôte local.

Mise à jour 2: Forcer la création de ASPNET_SessionId D'abord sur ma page login_load avant l'authentification avec OWIN.

1) avant de m'authentifier avec OWIN, je crée une valeur aléatoire Session.Content Sur ma page de connexion pour démarrer l'ASP.NET_SessionId 2) puis je m'authentifie et je fais d'autres sessions 3) D'autres navigateurs semblent fonctionner

C'est bizarre. Je ne peux que conclure que cela a quelque chose à voir avec ASP et OWIN pensant être dans des domaines différents ou quelque chose comme ça.

Mise à jour - Comportement étrange entre les deux.

Comportement étrange supplémentaire identifié - Timeout of Owin et ASP session est différente. Ce que je constate, c’est que mes sessions Owin restent en vie plus longtemps que mes ASP sessions via Ainsi, lors de la connexion: 1.) J'ai une session d'authentification basée sur un cookie 2.) Je règle quelques variables de session

Mes variables de session (2) "meurent" avant que la variable de session owin cookie force la reconnexion, ce qui entraîne un comportement inattendu dans l'ensemble de l'application. (La personne est connectée mais n'est pas vraiment connectée)

Mise à jour 3B

Après quelques recherches, une page indiquait que le délai d’authentification des formulaires et celui de la session devaient correspondre. Je pense que normalement les deux sont synchronisés, mais pour une raison quelconque, ils ne sont pas synchronisés.

Résumé des solutions de contournement

1) Créez toujours une session avant l’authentification. Généralement, créez une session lorsque vous démarrez l'application Session["Workaround"] = 0;

2) [Expérimental] si vous persistez les cookies, assurez-vous que votre délai d'attente/durée OWIN est plus long que votre sessionTimeout dans votre web.config (en cours de test)

135
Piotr Stulinski

J'ai rencontré le même problème et ai tracé la cause de l'implémentation d'hébergement OWIN ASP.NET. Je dirais que c'est un bug.

Un peu de fond

Mes conclusions sont basées sur ces versions de Assembly:

  • Microsoft.Owin, version = 2.0.2.0, Culture = neutre, PublicKeyToken = 31bf3856ad364e35
  • Microsoft.Owin.Host.SystemWeb, version = 2.0.2.0, Culture = neutre, PublicKeyToken = 31bf3856ad364e35
  • System.Web, Version = 4.0.0.0, Culture = neutre, PublicKeyToken = b03f5f7f11d50a3a

OWIN utilise sa propre abstraction pour travailler avec la réponse Cookies ( Microsoft.Owin.ResponseCookieCollection ). Cette implémentation enveloppe directement la collection d'en-têtes de réponse et met à jour en conséquence l'en-tête Set-Cookie . OWIN ASP.NET Host ( Microsoft.Owin.Host.SystemWeb ) encapsule simplement System.Web.HttpResponse et sa collection d'en-têtes. Ainsi, lorsqu'un nouveau cookie est créé via OWIN, l'en-tête de réponse Set-Cookie est modifié directement.

Mais ASP.NET utilise également sa propre abstraction pour travailler avec les cookies de réponse. Cela nous est présenté comme propriété System.Web.HttpResponse.Cookies et implémentée par la classe scellée System.Web.HttpCookieCollection . Cette implémentation n'enveloppe pas directement l'en-tête de la réponse Set-Cookie , mais utilise certaines optimisations et quelques notifications internes pour indiquer que l'état de l'objet de réponse a été modifié.

Ensuite, il y a un point tardif dans la durée de vie de la demande où HttpCookieCollection l'état modifié est testé ( System.Web.HttpResponse.GenerateResponseHeadersForCookies () ) et les cookies sont sérialisés. dans l'en-tête Set-Cookie . Si cette collection est dans un état spécifique, l'intégralité de l'en-tête Set-Cookie est d'abord effacée et recréée à partir des cookies stockés dans la collection.

L'implémentation de session ASP.NET utilise la propriété System.Web.HttpResponse.Cookies pour stocker le cookie ASP.NET_SessionId. Il existe également des optimisations de base dans le module d'état de session ASP.NET ( System.Web.SessionState.SessionStateModule ) implémenté via la propriété statique nommée s_sessionEverSet, qui se passe d'explication. Si vous stockez quelque chose comme état de session dans votre application, ce module effectuera un peu plus de travail pour chaque demande.


Retour à notre problème de connexion

Avec tous ces éléments, vos scénarios peuvent être expliqués.

Cas 1 - La session n'a jamais été définie

System.Web.SessionState.SessionStateModule , la propriété s_sessionEverSet est false. Aucun identifiant de session n'est généré par le module d'état de session et L'état de la collection System.Web.HttpResponse.Cookies est non détecté car modifié. Dans ce cas, les cookies OWIN sont correctement envoyés au navigateur et la connexion fonctionne.

Cas 2 - La session a été utilisée quelque part dans l'application, mais pas avant que l'utilisateur ne tente de s'authentifier

System.Web.SessionState.SessionStateModule , la propriété s_sessionEverSet est true. Les ID de session sont générés par SessionStateModule . ASP.NET_SessionId est ajouté à la collection System.Web.HttpResponse.Cookies , mais il est supprimé ultérieurement dans la durée de vie de la demande en tant qu'utilisateur. la session est en fait vide. Dans ce cas l'état de la collection System.Web.HttpResponse.Cookies est détecté comme ayant été modifié et en-tête Set-Cookie est d'abord effacé avant que les cookies ne soient sérialisés avec la valeur de l'en-tête.

Dans ce cas, les cookies de réponse OWIN sont "perdus" et l'utilisateur n'est pas authentifié et est redirigé vers la page de connexion.

Cas 3 - La session est utilisée avant que l'utilisateur ne tente de s'authentifier

System.Web.SessionState.SessionStateModule , la propriété s_sessionEverSet est true. Les identifiants de session sont générés par SessionStateModule . ASP.NET_SessionId est ajouté à System.Web.HttpResponse.Cookies . En raison de l'optimisation interne dans System.Web.HttpCookieCollection et System.Web.HttpResponse.GenerateResponseHeadersForCookies () L'en-tête Set-Cookie n'est PAS d'abord effacé mais seulement mis à jour.

Dans ce cas, les cookies d'authentification OWIN et le cookie ASP.NET_SessionId sont envoyés en réponse et la connexion fonctionne.


Problème plus général avec les cookies

Comme vous pouvez le constater, le problème est plus général et ne se limite pas à la session ASP.NET. Si vous hébergez OWIN via Microsoft.Owin.Host.SystemWeb et que vous/quelque chose utilisez directement la collection System.Web.HttpResponse.Cookies à risque.

Par exemple ça marche et les deux cookies sont correctement envoyés au navigateur ...

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";

    return View();
}

Mais ça ne marche pas et OwinCookie est "perdu" ...

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";
    HttpContext.Response.Cookies.Remove("ASPCookie");

    return View();
}

Tous deux testés à partir de VS2013, IISExpress et du modèle de projet MVC par défaut.

148
Tomas Dolezal

En commençant par l’excellente analyse de @TomasDolezal, j’ai jeté un coup d’œil aux sources Owin et System.Web.

Le problème est que System.Web a sa propre source principale d'informations sur les cookies et ce n'est pas l'en-tête Set-Cookie. Owin ne connaît que l’en-tête Set-Cookie. Une solution de contournement consiste à s'assurer que tous les cookies définis par Owin sont également définis dans la collection HttpContext.Current.Response.Cookies.

J'ai créé un petit middleware ( source , nuget ) qui fait exactement cela, qui est destiné à être placé immédiatement au-dessus de l'enregistrement du middleware de cookie.

app.UseKentorOwinCookieSaver();

app.UseCookieAuthentication(new CookieAuthenticationOptions());
40
Anders Abel

En bref, , le gestionnaire de cookies .NET l'emporte sur le gestionnaire de cookies OWIN et remplace les cookies définis sur la couche OWIN . Le correctif consiste à utiliser la classe SystemWebCookieManager, fournie comme solution sur le projet Katana ici . Vous devez utiliser cette classe ou une classe similaire, ce qui forcera OWIN à utiliser le gestionnaire de cookies .NET afin d'éviter toute incohérence :

public class SystemWebCookieManager : ICookieManager
{
    public string GetRequestCookie(IOwinContext context, string key)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
        var cookie = webContext.Request.Cookies[key];
        return cookie == null ? null : cookie.Value;
    }

    public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

        bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
        bool pathHasValue = !string.IsNullOrEmpty(options.Path);
        bool expiresHasValue = options.Expires.HasValue;

        var cookie = new HttpCookie(key, value);
        if (domainHasValue)
        {
            cookie.Domain = options.Domain;
        }
        if (pathHasValue)
        {
            cookie.Path = options.Path;
        }
        if (expiresHasValue)
        {
            cookie.Expires = options.Expires.Value;
        }
        if (options.Secure)
        {
            cookie.Secure = true;
        }
        if (options.HttpOnly)
        {
            cookie.HttpOnly = true;
        }

        webContext.Response.AppendCookie(cookie);
    }

    public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        AppendResponseCookie(
            context,
            key,
            string.Empty,
            new CookieOptions
            {
                Path = options.Path,
                Domain = options.Domain,
                Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            });
    }
}

Lors du démarrage de votre application, assignez-le simplement lorsque vous créez vos dépendances OWIN:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebCookieManager()
    ...
});

Une réponse similaire a été fournie ici, mais elle n'inclut pas tout le code-base nécessaire pour résoudre le problème. Je vois donc la nécessité de l'ajouter ici car le lien externe au projet Katana peut tomber et ceci devrait être entièrement décrit comme une solution ici aussi.

31
Alexandru

L'équipe Katana a répondu au numéro Tomas Dolezar a soulevé et posté documentation sur les solutions de contournement :

Les solutions de contournement se divisent en deux catégories. L'une consiste à reconfigurer System.Web afin d'éviter l'utilisation de la collection Response.Cookies et le remplacement des cookies OWIN. L'autre approche consiste à reconfigurer les composants OWIN concernés afin qu'ils écrivent des cookies directement dans la collection Response.Cookies de System.Web.

  • Assurez-vous que la session est établie avant l'authentification: le conflit entre les cookies System.Web et Katana se fait par requête. Il est donc possible que l'application établisse la session sur une requête antérieure au flux d'authentification. Cela devrait être facile à faire lorsque l'utilisateur arrive pour la première fois, mais il peut être plus difficile de garantir ultérieurement que les cookies de session ou d'authentification expirent et/ou doivent être actualisés.
  • Désactiver le SessionStateModule - Si l'application ne s'appuie pas sur des informations de session, mais que le module de session crée toujours un cookie qui provoque le conflit ci-dessus, vous pouvez envisager de désactiver le module d'état de session.
  • Reconfigurez le CookieAuthenticationMiddleware pour écrire directement dans la collection de cookies de System.Web.
app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

Voir Implémentation de SystemWebCookieManager dans la documentation (lien ci-dessus).

Plus d'informations ici

Modifier

Ci-dessous les mesures que nous avons prises pour résoudre le problème. Les deux problèmes résolus ont également été résolus séparément, mais nous avons décidé de les appliquer au cas où:

1. Utilisez SystemWebCookieManager

2. Définissez la variable de session:

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // See http://stackoverflow.com/questions/20737578/asp-net-sessionid-owin-cookies-do-not-send-to-browser/
    requestContext.HttpContext.Session["FixEternalRedirectLoop"] = 1;
}

Remarque: la méthode Initialize ci-dessus est l'emplacement logique du correctif, car base.Initialize rend la session disponible. Toutefois, le correctif peut également être appliqué ultérieurement, car OpenId contient d'abord une demande anonyme, puis redirige vers le fournisseur OpenId, puis de nouveau. Les problèmes surviendraient après la redirection vers l’application pendant que le correctif définit déjà la variable de session lors de la première demande anonyme, corrigeant ainsi le problème avant qu’une redirection ne se produise, même)

Éditer 2

Copiez-collez à partir du projet Katana 2016-05-14:

Ajoute ça:

app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

...et ça:

public class SystemWebCookieManager : ICookieManager
{
    public string GetRequestCookie(IOwinContext context, string key)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
        var cookie = webContext.Request.Cookies[key];
        return cookie == null ? null : cookie.Value;
    }

    public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

        bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
        bool pathHasValue = !string.IsNullOrEmpty(options.Path);
        bool expiresHasValue = options.Expires.HasValue;

        var cookie = new HttpCookie(key, value);
        if (domainHasValue)
        {
            cookie.Domain = options.Domain;
        }
        if (pathHasValue)
        {
            cookie.Path = options.Path;
        }
        if (expiresHasValue)
        {
            cookie.Expires = options.Expires.Value;
        }
        if (options.Secure)
        {
            cookie.Secure = true;
        }
        if (options.HttpOnly)
        {
            cookie.HttpOnly = true;
        }

        webContext.Response.AppendCookie(cookie);
    }

    public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        AppendResponseCookie(
            context,
            key,
            string.Empty,
            new CookieOptions
            {
                Path = options.Path,
                Domain = options.Domain,
                Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            });
    }
}
16
thomius

Des réponses ont déjà été fournies, mais dans owin 3.1.0, une classe SystemWebChunkingCookieManager peut être utilisée.

https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs

https://raw.githubusercontent.com/aspnet/AspNetKatana/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin.Host.SystemWeb/SystemWeb/SystemWeb/SystemWebChunkingCookieManager.cs

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebChunkingCookieManager()
    ...
});
3
jonmeyer

Si vous définissez vous-même les cookies dans le middleware OWIN, alors utiliser OnSendingHeaders semble résoudre le problème.

Par exemple, en utilisant le code ci-dessous owinResponseCookie2 sera défini même si owinResponseCookie1 n'est pas:

private void SetCookies()
{
    var owinContext = HttpContext.GetOwinContext();
    var owinResponse = owinContext.Response;

    owinResponse.Cookies.Append("owinResponseCookie1", "value1");

    owinResponse.OnSendingHeaders(state =>
    {
        owinResponse.Cookies.Append("owinResponseCookie2", "value2");
    },
    null);

    var httpResponse = HttpContext.Response;
    httpResponse.Cookies.Remove("httpResponseCookie1");
}
3
Appetere

La solution de code à une ligne la plus rapide:

HttpContext.Current.Session["RunSession"] = "1";

Ajoutez simplement cette ligne avant la méthode CreateIdentity:

HttpContext.Current.Session["RunSession"] = "1";
var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberLogin }, userIdentity);
2
Alexander Trofimov

J'ai eu le même symptôme de l'en-tête Set-Cookie non envoyé mais aucune de ces réponses ne m'a aidé. Tout fonctionnait sur ma machine locale mais, une fois déployés en production, les en-têtes set-cookie ne seraient jamais configurés.

Il s’avère que c’est une combinaison de l’utilisation d’un CookieAuthenticationMiddleware personnalisé avec WebApi avec support de la compression WebApi

Heureusement, j'utilisais ELMAH dans mon projet, ce qui m'a permis de noter cette exception:

System.Web.HttpException Server ne peut pas ajouter d'en-tête après l'envoi des en-têtes HTTP.

Ce qui m'a amené à ceci GitHub Issue

En gros, si vous avez une configuration étrange comme la mienne, vous voudrez désactiver la compression pour vos contrôleurs WebApi/méthodes qui configurent les cookies, ou essayez le OwinServerCompressionHandler.

1
Hugh Jeffner