ASP.NET_SessionId + OWIN Cookies não são enviados para o navegador

Eu tenho um problema estranho com o uso de autenticação de cookies do Owin.

Quando inicio a autenticação do servidor IIS, funciona perfeitamente no IE / Firefox e no Chrome.

Eu comecei a fazer alguns testes com autenticação e logar em diferentes plataformas e descobri um erro estranho. Esporadicamente, o framework Owin / IIS simplesmente não envia cookies para os navegadores. Eu vou digitar um nome de usuário e senha que está correto o código é executado, mas nenhum cookie é entregue ao navegador em tudo. Se eu reiniciar o servidor, ele começará a funcionar e, em algum momento, tentarei fazer o login e, novamente, os cookies deixarão de ser entregues. Pisando sobre o código não faz nada e não gera erros.

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

E dentro do meu procedimento de login, eu tenho o seguinte 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); 

Atualização 1: Parece que uma das causas do problema é quando eu adiciono itens à session e os problemas começam. Adicionar algo simples como Session.Content["ABC"]= 123 parece criar o problema.

O que eu posso entender é o seguinte: 1) (Chrome) Quando eu faço o login, recebo o ASP.NET_SessionId + meu cookie de autenticação. 2) Eu vou para uma página que define um session.contents … 3) Abra um novo navegador (Firefox) e tente login e ele não recebe um ASP.NET_SessionId nem obtém um Authentication Cookie 4) Enquanto o primeiro navegador tem o ASP.NET_SessionId continua a funcionar. O minuto que eu remover este cookie tem o mesmo problema que todos os outros navegadores estou trabalhando em endereço IP (10.xxx) e localhost.

Atualização 2: forçar a criação de ASPNET_SessionId primeiro na minha página login_load antes da autenticação com o OWIN.

1) antes de eu autenticar com OWIN Eu faço um valor random Session.Content na minha página de login para iniciar o ASP.NET_SessionId 2) então eu autentico e faço outras sessões 3) Outros navegadores parecem agora funcionar

Isso é bizarro. Eu só posso concluir que isso tem algo a ver com ASP e OWIN pensando que eles estão em domínios diferentes ou algo parecido.

Atualização 3 – Comportamento estranho entre os dois.

Comportamento estranho adicional identificado – Tempo limite de Owin e session ASP é diferente. O que estou vendo é que minhas sessões de Owin estão permanecendo vivas por mais tempo que minhas sessões ASP através de algum mecanismo. Então, ao fazer o login: 1.) Eu tenho uma session de autenticação baseada cookies 2.) Eu defino algumas variables ​​de session

Minhas variables ​​de session (2) “morrer” antes da variável de session do cookie owin forçam o login novamente, o que causa um comportamento inesperado em todo o meu aplicativo inteiro. (A pessoa está logada, mas não está realmente conectada)

Atualização 3B

Depois de algumas pesquisas, vi alguns comentários em uma página dizendo que o tempo limite de autenticação de “formulários” e o tempo limite da session precisam ser correspondentes. Eu estou pensando normalmente que os dois estão em sincronia, mas por alguma razão os dois não estão em sincronia.

Resumo de soluções alternativas

1) Sempre crie uma session antes da autenticação. Basicamente criar session quando você iniciar o aplicativo Session["Workaround"] = 0;

2) [Experimental] se você persistir os cookies, certifique-se de que seu tempo / duração de OWIN seja maior que seu sessionTimeout no seu web.config (em teste)

   

    Eu encontrei o mesmo problema e tracei a causa para a implementação de hospedagem OWIN ASP.NET. Eu diria que é um bug.

    Algum fundo

    Minhas descobertas são baseadas nessas versões de assembly:

    • Microsoft.Owin, versão = 2.0.2.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35
    • Microsoft.Owin.Host.SystemWeb, Version = 2.0.2.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35
    • System.Web, versão = 4.0.0.0, Culture = neutral, PublicKeyToken = b03f5f7f11d50a3a

    OWIN usa sua própria abstração para trabalhar com cookies de resposta ( Microsoft.Owin.ResponseCookieCollection ). Essa implementação encapsula diretamente a coleção de headers de resposta e, consequentemente, atualiza o header Set-Cookie . O host OWIN ASP.NET ( Microsoft.Owin.Host.SystemWeb ) apenas envolve o System.Web.HttpResponse e sua coleção de headers. Portanto, quando um novo cookie é criado por meio do OWIN, o header do conjunto de respostas é alterado diretamente.

    Mas o ASP.NET também usa sua própria abstração para trabalhar com cookies de resposta. Isso é exposto a nós como propriedade System.Web.HttpResponse.Cookies e implementado pela class sealed System.Web.HttpCookieCollection . Essa implementação não envolve o header de resposta Set-Cookie diretamente, mas usa algumas otimizações e várias notifications internas para manifestar seu estado alterado para o object de resposta.

    Em seguida, há um ponto final no tempo de vida da solicitação em que o estado alterado HttpCookieCollection é testado ( System.Web.HttpResponse.GenerateResponseHeadersForCookies () ) e os cookies são serializados para o header Set-Cookie . Se esta coleção estiver em algum estado específico, todo o header Set-Cookie será primeiro limpo e recriado a partir de cookies armazenados na coleção.

    A implementação de session ASP.NET usa a propriedade System.Web.HttpResponse.Cookies para armazenar seu cookie ASP.NET_SessionId. Além disso, há alguma otimização básica no módulo de estado de session do ASP.NET ( System.Web.SessionState.SessionStateModule ) implementada por meio da propriedade estática denominada s_sessionEverSet, que é bastante autoexplicativa. Se você já armazenou algo no estado da session em seu aplicativo, este módulo fará um pouco mais de trabalho para cada solicitação.


    De volta ao nosso problema de login

    Com todas essas peças, seus cenários podem ser explicados.

    Caso 1 – Sessão nunca foi definida

    System.Web.SessionState.SessionStateModule , a propriedade s_sessionEverSet é falsa. Nenhuma identificação de session é gerada pelo módulo de estado da session e o estado da coleção System.Web.HttpResponse.Cookies não é detectado como alterado . Neste caso, os cookies OWIN são enviados corretamente para o navegador e o login funciona.

    Caso 2 – A session foi usada em algum lugar do aplicativo, mas não antes que o usuário tente autenticar

    System.Web.SessionState.SessionStateModule , a propriedade s_sessionEverSet é verdadeira. Os Ids da session são gerados pelo SessionStateModule , o ASP.NET_SessionId é adicionado à coleção System.Web.HttpResponse.Cookies, mas é removido posteriormente no tempo de vida da solicitação, já que a session do usuário está vazia. Nesse caso, o estado da coleção System.Web.HttpResponse.Cookies é detectado como alterado e o header Set-Cookie é primeiro limpo antes que os cookies sejam serializados para o valor do header.

    Neste caso, os cookies de resposta OWIN são “perdidos” e o usuário não é autenticado e é redirecionado de volta para a página de login.

    Caso 3 – A session é usada antes que o usuário tente autenticar

    System.Web.SessionState.SessionStateModule , a propriedade s_sessionEverSet é verdadeira. Ids de session são gerados por SessionStateModule , ASP.NET_SessionId é adicionado ao System.Web.HttpResponse.Cookies . Devido à otimização interna em System.Web.HttpCookieCollection e System.Web.HttpResponse.GenerateResponseHeadersForCookies () , o header Set-Cookie NÃO é primeiro desmarcado, mas apenas atualizado.

    Nesse caso, os cookies de autenticação OWIN e o cookie ASP.NET_SessionId são enviados em trabalhos de resposta e login.


    Problema mais geral com cookies

    Como você pode ver, o problema é mais geral e não está limitado à session do ASP.NET. Se você estiver hospedando o OWIN através do Microsoft.Owin.Host.SystemWeb e você / alguma coisa estiver usando diretamente a coleção System.Web.HttpResponse.Cookies, você estará em risco.

    Por exemplo, isso funciona e ambos os cookies são enviados corretamente para o navegador …

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

    Mas isso não e OwinCookie é “perdido” …

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

    Ambos testados a partir do VS2013, do IISExpress e do modelo de projeto padrão do MVC.

    Começando com a ótima análise do @TomasDolezal, eu dei uma olhada na fonte Owin e System.Web.

    O problema é que System.Web tem sua própria fonte mestra de informações de cookie e que não é o header Set-Cookie. Owin só sabe sobre o header Set-Cookie. Uma solução é certificar-se de que quaisquer cookies definidos pelo Owin também sejam configurados na coleção HttpContext.Current.Response.Cookies .

    Eu fiz um pequeno middleware ( source , nuget ) que faz exatamente isso, que é destinado a ser colocado imediatamente acima do registro do middleware do cookie.

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

    Em suma, o gerenciador de cookies .NET conquistará o gerenciador de cookies OWIN e replaceá os cookies definidos na camada OWIN . A correção é usar a class SystemWebCookieManager, fornecida como uma solução no Projeto Katana aqui . Você precisa usar essa class ou uma semelhante a ela, o que forçará o OWIN a usar o gerenciador de cookies do .NET para que não haja inconsistências :

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

    Na boot de seu aplicativo, basta atribuí-lo quando você criar suas dependencies OWIN:

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

    Uma resposta semelhante foi fornecida aqui, mas não inclui toda a base de código necessária para resolver o problema, por isso vejo a necessidade de incluí-la aqui porque o link externo para o Projeto Katana pode ficar inativo e isso deve ser totalmente registrado como uma solução aqui também.

    A equipe do Katana respondeu à questão levantada por Tomas Dolezar e publicou documentação sobre soluções alternativas :

    Soluções alternativas caem em duas categorias. Uma delas é reconfigurar o System.Web de modo que ele evite usar a coleção Response.Cookies e sobrescrever os cookies OWIN. A outra abordagem é reconfigurar os componentes OWIN afetados para que eles gravem cookies diretamente na coleção Response.Cookies do System.Web.

    • Assegure-se de que a session seja estabelecida antes da autenticação: O conflito entre os cookies System.Web e Katana é por solicitação, portanto, é possível que o aplicativo estabeleça a session em alguma solicitação antes do stream de autenticação. Isso deve ser fácil quando o usuário chega pela primeira vez, mas pode ser mais difícil garantir mais tarde quando a session ou os cookies de autenticação expiram e / ou precisam ser atualizados.
    • Desativar o SessionStateModule – Se o aplicativo não estiver confiando nas informações da session, mas o módulo de session ainda estiver configurando um cookie que causa o conflito acima, você poderá considerar a desativação do módulo de estado da session.
    • Reconfigure o CookieAuthenticationMiddleware para gravar diretamente na coleção de cookies do System.Web.
     app.UseCookieAuthentication(new CookieAuthenticationOptions { // ... CookieManager = new SystemWebCookieManager() }); 

    Veja a implementação de SystemWebCookieManager na documentação (link acima)

    Mais informações aqui

    Editar

    Abaixo os passos que tomamos para resolver o problema. Ambos 1. e 2. resolvido o problema também separadamente, mas decidimos aplicar ambos apenas no caso:

    1. Use SystemWebCookieManager

    2. Defina a variável da 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; } 

    (Nota: o método Initialize acima é o lugar lógico para a correção porque base.Initialize disponibiliza Session. No entanto, a correção também pode ser aplicada mais tarde porque no OpenId há primeiro uma solicitação anônima, redirect para o provedor OpenId e depois voltar Os problemas ocorreriam após o redirecionamento de volta para o aplicativo, enquanto a correção define a variável de session já durante a primeira solicitação anônima, corrigindo assim o problema antes que qualquer redirecionamento volte a acontecer)

    Editar 2

    Copie e cole do projeto Katana 2016-05-14:

    Adicione isso:

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

    …e isto:

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

    As respostas já foram fornecidas, mas no owin 3.1.0, há uma class SystemWebChunkingCookieManager que pode ser usada.

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

    Se você está configurando cookies no middleware OWIN, então usar OnSendingHeaders parece contornar o problema.

    Por exemplo, usar o código abaixo owinResponseCookie2 será definido, mesmo que owinResponseCookie1 não seja:

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

    A solução de código de uma linha mais rápida:

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

    Basta adicionar essa linha antes do método CreateIdentity:

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

    Eu tive o mesmo sintoma do header Set-Cookie não sendo enviado, mas nenhuma dessas respostas me ajudou. Tudo funcionou na minha máquina local, mas quando implantado na produção, os headers de cookie de conjunto nunca seriam configurados.

    Acontece que foi uma combinação de usar um CookieAuthenticationMiddleware personalizado com o WebApi junto com o suporte à compactação WebApi

    Por sorte, eu estava usando o ELMAH no meu projeto, o que me permitiu registrar essa exceção:

    System.Web.HttpException O servidor não pode append o header após os headers HTTP terem sido enviados.

    O que me levou a essa questão do GitHub

    Basicamente, se você tem uma configuração estranha como a minha, você vai querer desabilitar a compression para seus controladores WebApi / methods que definem cookies, ou tente o OwinServerCompressionHandler .