Las cookies de ASP.NET_SessionId + OWIN no se envían al navegador

Tengo un problema extraño con el uso de la autenticación de cookies Owin.

Cuando inicio mi servidor IIS, la autenticación funciona perfectamente en IE / Firefox y Chrome.

Comencé a hacer algunas pruebas con Autenticación e iniciando sesión en diferentes plataformas y he encontrado un error extraño. Esporádicamente, Owin framework / IIS simplemente no envía cookies a los navegadores. Escribiré un nombre de usuario y una contraseña correctos para que se ejecute el código, pero no se entrega ninguna cookie al navegador. Si reinicio el servidor, comienza a funcionar y en algún momento intentaré iniciar sesión y de nuevo las cookies dejan de ser entregadas. Pasar el código no hace nada y no arroja errores.

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

Y dentro de mi procedimiento de inicio de sesión tengo el siguiente código:

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

Actualización 1: Parece que una de las causas del problema es que cuando agrego elementos a la sesión comienzan los problemas. Agregar algo simple como Session.Content["ABC"]= 123 parece crear el problema.

Lo que puedo distinguir es lo siguiente: 1) (Chrome) Cuando inicio sesión recibo ASP.NET_SessionId + mi cookie de autenticación. 2) Voy a una página que establece una sesión.contenidos … 3) Abra un nuevo navegador (Firefox) e intente iniciar sesión y no reciba un ASP.NET_SessionId ni obtenga una Cookie de autenticación 4) Mientras que el primer navegador tiene ASP.NET_SessionId, continúa funcionando. En el momento en que elimino esta cookie, tiene el mismo problema que todos los otros navegadores en los que estoy trabajando: dirección IP (10.xxx) y localhost.

Actualización 2: Forzar la creación de ASPNET_SessionId primero en mi página login_load antes de la autenticación con OWIN.

1) antes de autenticarme con OWIN, realizo un valor aleatorio de Session.Content en mi página de inicio de sesión para iniciar ASP.NET_SessionId 2) luego me autentico y realizo más sesiones 3) Otros navegadores parecen funcionar ahora

Esto es extraño Solo puedo concluir que esto tiene algo que ver con ASP y OWIN pensando que están en dominios diferentes o algo así.

Actualización 3 – Comportamiento extraño entre los dos.

Se identificó un comportamiento extraño adicional: el tiempo de espera de Owin y la sesión ASP es diferente. Lo que estoy viendo es que mis sesiones de Owin se mantienen activas más tiempo que mis sesiones ASP a través de algún mecanismo. Entonces al iniciar sesión: 1.) Tengo una sesión de auth basada en cookied 2.) Configuré algunas variables de sesión

Mis variables de sesión (2) “mueren” antes de la variable de sesión de cookie owin obliga a volver a iniciar sesión, lo que provoca un comportamiento inesperado en toda mi aplicación. (La persona ha iniciado sesión pero realmente no ha iniciado sesión)

Actualización 3B

Después de algunas excavaciones vi algunos comentarios en una página que dicen que el tiempo de espera de autenticación de “formularios” y el tiempo de espera de la sesión deben coincidir. Estoy pensando que normalmente los dos están sincronizados, pero por alguna razón los dos no están sincronizados.

Resumen de soluciones provisionales

1) Siempre crea una sesión primero antes de la autenticación. Básicamente, cree la sesión cuando inicie la aplicación Session["Workaround"] = 0;

2) [Experimental] si persiste en las cookies, asegúrese de que su tiempo de espera OWIN sea más largo que su sessionTimeout en su web.config (en prueba)

He encontrado el mismo problema y he rastreado la causa de la implementación de alojamiento de OWIN ASP.NET. Yo diría que es un error.

Algunos antecedentes

Mis hallazgos se basan en estas versiones de ensamblaje:

  • Microsoft.Owin, Versión = 2.0.2.0, Cultura = neutral, PublicKeyToken = 31bf3856ad364e35
  • Microsoft.Owin.Host.SystemWeb, Versión = 2.0.2.0, Cultura = neutral, PublicKeyToken = 31bf3856ad364e35
  • System.Web, Versión = 4.0.0.0, Cultura = neutral, PublicKeyToken = b03f5f7f11d50a3a

OWIN usa su propia abstracción para trabajar con Cookies de respuesta ( Microsoft.Owin.ResponseCookieCollection ). Esta implementación envuelve directamente la colección de encabezados de respuesta y, en consecuencia, actualiza el encabezado Set-Cookie . El host OWIN ASP.NET ( Microsoft.Owin.Host.SystemWeb ) simplemente envuelve System.Web.HttpResponse y su colección de encabezados. Entonces, cuando se crea una nueva cookie a través de OWIN, el encabezado Set-Cookie de respuesta se cambia directamente.

Pero ASP.NET también usa su propia abstracción para trabajar con Cookies de respuesta. Esto está expuesto a nosotros como propiedad de System.Web.HttpResponse.Cookies e implementado por la clase sellada System.Web.HttpCookieCollection . Esta implementación no envuelve el encabezado Set-Cookie de respuesta directamente, pero usa algunas optimizaciones y un puñado de notificaciones internas para manifestar que ha cambiado el estado al objeto de respuesta.

Luego hay un punto tarde en la vida de la solicitud donde se prueba el estado cambiado de HttpCookieCollection ( System.Web.HttpResponse.GenerateResponseHeadersForCookies () ) y las cookies se serializan al encabezado Set-Cookie . Si esta colección está en un estado específico, el encabezado completo de Set-Cookie primero se borra y se recrea a partir de las cookies almacenadas en la colección.

La implementación de la sesión ASP.NET utiliza la propiedad System.Web.HttpResponse.Cookies para almacenar su cookie ASP.NET_SessionId. También hay una optimización básica en el módulo de estado de sesión de ASP.NET ( System.Web.SessionState.SessionStateModule ) implementado a través de la propiedad estática llamada s_sessionEverSet que se explica por sí misma. Si alguna vez almacena algo en estado de sesión en su aplicación, este módulo hará un poco más de trabajo para cada solicitud.


Volver a nuestro problema de inicio de sesión

Con todas estas piezas tus escenarios pueden explicarse.

Caso 1: la sesión nunca se configuró

La propiedad System.Web.SessionState.SessionStateModule , s_sessionEverSet es falsa. El módulo de estado de la sesión no genera identificadores de sesión y el estado de recostackción System.Web.HttpResponse.Cookies no se detecta como modificado . En este caso, las cookies OWIN se envían correctamente al navegador y el inicio de sesión funciona.

Caso 2: la sesión se usó en algún lugar de la aplicación, pero no antes de que el usuario intente autenticarse

System.Web.SessionState.SessionStateModule , la propiedad s_sessionEverSet es verdadera. Los ID de sesión son generados por SessionStateModule , ASP.NET_SessionId se agrega a la colección System.Web.HttpResponse.Cookies , pero se elimina más adelante durante el ciclo de solicitud, ya que la sesión del usuario está vacía. En este caso, el estado de recostackción System.Web.HttpResponse.Cookies se detecta como cambiado y el encabezado Set-Cookie se borra antes de que las cookies se serialicen al valor del encabezado.

En este caso, las cookies de respuesta OWIN se “pierden” y el usuario no se autentica y se redirige a la página de inicio de sesión.

Caso 3: la sesión se usa antes de que el usuario intente autenticarse

System.Web.SessionState.SessionStateModule , la propiedad s_sessionEverSet es verdadera. Los ID de sesión son generados por SessionStateModule , ASP.NET_SessionId se agrega a System.Web.HttpResponse.Cookies . Debido a la optimización interna en System.Web.HttpCookieCollection y System.Web.HttpResponse.GenerateResponseHeadersForCookies () , el encabezado Set-Cookie NO se borra primero, pero solo se actualiza.

En este caso, las cookies de autenticación OWIN y la cookie ASP.NET_SessionId se envían en respuesta y el inicio de sesión funciona.


Un problema más general con las cookies

Como puede ver, el problema es más general y no se limita a la sesión de ASP.NET. Si aloja OWIN a través de Microsoft.Owin.Host.SystemWeb y usted / algo está utilizando directamente la colección System.Web.HttpResponse.Cookies está en riesgo.

Por ejemplo, esto funciona y las dos cookies se envían correctamente al navegador …

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

Pero esto no es así y OwinCookie está “perdida” …

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

Ambos probaron desde VS2013, IISExpress y la plantilla de proyecto MVC predeterminada.

Comenzando con el gran análisis de @TomasDolezal, eché un vistazo a las fonts de Owin y System.Web.

El problema es que System.Web tiene su propia fuente principal de información de cookies y ese no es el encabezado Set-Cookie. Owin solo conoce el encabezado Set-Cookie. Una solución consiste en asegurarse de que las cookies establecidas por Owin también se establezcan en la colección HttpContext.Current.Response.Cookies .

He creado un pequeño middleware ( fuente , nuget ) que hace exactamente eso, que está destinado a colocarse inmediatamente encima del registro del middleware de cookies.

 app.UseKentorOwinCookieSaver(); app.UseCookieAuthentication(new CookieAuthenticationOptions()); 

En resumen, el administrador de cookies .NET ganará el administrador de cookies OWIN y sobrescribirá las cookies establecidas en la capa OWIN . La solución es usar la clase SystemWebCookieManager, proporcionada como una solución en el Proyecto Katana aquí . Debe usar esta clase o una similar, lo que obligará a OWIN a usar el administrador de cookies .NET para que no haya incoherencias :

 public class SystemWebCookieManager : ICookieManager { public string GetRequestCookie(IOwinContext context, string key) { if (context == null) { throw new ArgumentNullException("context"); } var webContext = context.Get(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(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), }); } } 

En el inicio de su aplicación, simplemente asigne cuando cree sus dependencias OWIN:

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

Se ha proporcionado una respuesta similar aquí, pero no incluye toda la base de código necesaria para resolver el problema, por lo que veo la necesidad de agregarla aquí porque el enlace externo al Proyecto Katana puede estar inactivo y esto debe ser una crónica completa. como una solución aquí también.

El equipo de Katana respondió al problema planteado por Tomas Dolezar y publicó documentación sobre soluciones alternativas :

Las soluciones temporales se dividen en dos categorías. Una es volver a configurar System.Web para evitar el uso de la colección Response.Cookies y sobrescribir las cookies de OWIN. El otro enfoque es volver a configurar los componentes afectados de OWIN para que escriban las cookies directamente en la colección Response.Cookies de System.Web.

  • Asegúrese de que la sesión esté establecida antes de la autenticación: el conflicto entre las cookies de System.Web y Katana es por solicitud, por lo que la aplicación puede establecer la sesión en alguna solicitud antes del flujo de autenticación. Esto debería ser fácil de hacer cuando el usuario llega por primera vez, pero puede ser más difícil de garantizar más adelante cuando la sesión o las cookies de autenticación caduquen y / o necesiten actualizarse.
  • Deshabilite el SessionStateModule: si la aplicación no se basa en la información de la sesión, pero el módulo de la sesión sigue configurando una cookie que causa el conflicto anterior, puede considerar deshabilitar el módulo de estado de la sesión.
  • Vuelva a configurar CookieAuthenticationMiddleware para que escriba directamente en la colección de cookies de System.Web.
 app.UseCookieAuthentication(new CookieAuthenticationOptions { // ... CookieManager = new SystemWebCookieManager() }); 

Ver la implementación de SystemWebCookieManager desde la documentación (enlace de arriba)

Más información aquí

Editar

Debajo de los pasos que tomamos para resolver el problema. Tanto 1. como 2. resolvieron el problema también por separado, pero decidimos aplicar ambos por si acaso:

1. Use SystemWebCookieManager

2. Configure la variable de sesión:

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

(nota al margen: el método de inicialización anterior es el lugar lógico para la corrección porque base.Initialize hace que la sesión esté disponible. Sin embargo, la corrección también podría aplicarse más adelante porque en OpenId hay primero una solicitud anónima, luego redirecciona al proveedor de OpenId y luego vuelve a la aplicación. Los problemas ocurrirían después de la redirección a la aplicación, mientras que la solución ya establece la variable de sesión durante la primera solicitud anónima, lo que soluciona el problema antes de que ocurra una redirección de regreso)

Editar 2

Copiar y pegar del proyecto Katana 2016-05-14:

Agrega esto:

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

…y esto:

 public class SystemWebCookieManager : ICookieManager { public string GetRequestCookie(IOwinContext context, string key) { if (context == null) { throw new ArgumentNullException("context"); } var webContext = context.Get(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(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), }); } } 

Las respuestas ya se han proporcionado, pero en owin 3.1.0, hay una clase SystemWebChunkingCookieManager que se puede utilizar.

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/SystemWebChunkingCookieManager.cs

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

Si está configurando cookies en middleware OWIN usted mismo, el uso de OnSendingHeaders parece resolver el problema.

Por ejemplo, se establecerá el uso del siguiente código owinResponseCookie2 , aunque owinResponseCookie1 no es:

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

La solución de código de una línea más rápida:

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

Simplemente agregue esta línea antes del método CreateIdentity:

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

Tenía el mismo síntoma de que el encabezado de Set-Cookie no se enviaba, pero ninguna de estas respuestas me ayudó. Todo funcionó en mi máquina local, pero cuando se implementó en producción, los encabezados de cookies establecidas nunca se configuraron.

Resulta que fue una combinación de usar un CookieAuthenticationMiddleware personalizado con WebApi junto con el soporte de compresión WebApi

Afortunadamente, estaba usando ELMAH en mi proyecto, lo que me permitió que esta excepción se registrara:

System.Web.HttpException Server no puede agregar encabezado después de que se hayan enviado encabezados HTTP.

Lo que me llevó a este número de GitHub

Básicamente, si tiene una configuración extraña como la mía, querrá deshabilitar la compresión para los controladores / métodos de WebApi que configuran las cookies, o pruebe OwinServerCompressionHandler .