ASP.NET_SessionId + OWIN I cookie non vengono inviati al browser

Ho uno strano problema con l’utilizzo dell’autenticazione dei cookie Owin.

Quando avvio l’autenticazione del server IIS funziona perfettamente su IE / Firefox e Chrome.

Ho iniziato a fare alcuni test con l’autenticazione e ad accedere su piattaforms diverse e ho trovato uno strano errore. Sporadicamente il framework Owin / IIS non invia alcun cookie ai browser. Inserirò un nome utente e una password corretti, ma il codice viene eseguito ma nessun cookie viene consegnato al browser. Se riavvio il server inizia a funzionare, a un certo punto proverò ad accedere e di nuovo i cookie smetteranno di essere consegnati. Passare il codice non fa nulla e non genera errori.

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 all’interno della mia procedura di accesso ho il seguente codice:

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

Aggiornamento 1: Sembra che una causa del problema è quando aggiungo gli elementi alla sessione che iniziano i problemi. Aggiungere qualcosa di semplice come Session.Content["ABC"]= 123 sembra creare il problema.

Quello che riesco a capire è il seguente: 1) (Chrome) Quando accedo ricevo ASP.NET_SessionId + il mio cookie di autenticazione. 2) Vado a una pagina che imposta una session.contents … 3) Apri un nuovo browser (Firefox) e prova ad accedere e non riceve un ASP.NET_SessionId né ottiene un cookie di autenticazione 4) Mentre il primo browser ha ASP.NET_SessionId continua a funzionare. Nel momento in cui rimuovo questo cookie ha lo stesso problema di tutti gli altri browser che sto lavorando su indirizzo ip (10.xxx) e localhost.

Aggiornamento 2: Forza la creazione di ASPNET_SessionId prima sulla mia pagina login_load prima dell’autenticazione con OWIN.

1) prima di autenticarmi con OWIN, creo un valore Session.Content casuale sulla mia pagina di accesso per avviare ASP.NET_SessionId 2), quindi autenticarsi e effettuare ulteriori sessioni 3) Altri browser sembrano funzionare ora

Questo è bizzarro. Posso solo concludere che questo ha qualcosa a che fare con ASP e OWIN pensando che siano in domini diversi o qualcosa del genere.

Aggiornamento 3 – Strano comportamento tra i due.

Identificazione di un comportamento strano aggiuntivo – Il timeout di Owin e la sessione ASP sono diversi. Quello che sto vedendo è che le mie sessioni di Owin si mantengono in vita più a lungo delle mie sessioni ASP attraverso qualche meccanismo. Quindi quando effettui il login: 1.) Ho una sessione di autenticazione basata su cooky 2.) Ho impostato alcune variabili di sessione

Le mie variabili di sessione (2) “muoiono” prima che la variabile della sessione di cookie di Owin imponga il re-login, il che provoca un comportamento imprevisto in tutta la mia intera applicazione. (La persona ha effettuato l’accesso ma non ha effettuato l’accesso)

Aggiornamento 3B

Dopo alcuni scavi ho visto alcuni commenti su una pagina che diceva che il timeout di autenticazione dei “moduli” e il timeout della sessione devono corrispondere. Sto pensando normalmente che i due siano sincronizzati, ma per qualche ragione i due non sono sincronizzati.

Riepilogo di soluzioni alternative

1) Crea sempre una sessione prima di autenticare. Fondamentalmente crea una sessione all’avvio dell’applicazione Session["Workaround"] = 0;

2) [Sperimentale] se i cookie persistono assicurati che il tuo timeout / lunghezza OWIN sia più lungo della sessione Timeout nel tuo web.config (in testing)

Ho riscontrato lo stesso problema e ho rintracciato la causa dell’implementazione di hosting di ASP.NET OWIN. Direi che è un bug.

Qualche sfondo

Le mie scoperte sono basate su queste versioni di assemblaggio:

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

OWIN usa la propria astrazione per lavorare con i cookie di risposta ( Microsoft.Owin.ResponseCookieCollection ). Questa implementazione avvolge direttamente la raccolta di intestazioni di risposta e aggiorna di conseguenza l’intestazione Set-Cookie . L’host OWIN ASP.NET ( Microsoft.Owin.Host.SystemWeb ) include semplicemente System.Web.HttpResponse e la sua raccolta di intestazioni. Quindi, quando il nuovo cookie viene creato tramite OWIN, l’intestazione Set-Cookie di risposta viene modificata direttamente.

Ma ASP.NET usa anche la propria astrazione per lavorare con i cookie di risposta. Questo è esposto a noi come proprietà System.Web.HttpResponse.Cookies e implementato dalla class sealed System.Web.HttpCookieCollection . Questa implementazione non include direttamente l’intestazione Set-Cookie della risposta, ma utilizza alcune ottimizzazioni e una manciata di notifiche interne per manifestare lo stato modificato in object risposta.

Poi c’è un punto in ritardo nella durata della richiesta in cui lo stato modificato di HttpCookieCollection è stato testato ( System.Web.HttpResponse.GenerateResponseHeadersForCookies () ) ei cookie sono serializzati nell’intestazione Set-Cookie . Se questa raccolta è in uno stato specifico, l’intera intestazione Set-Cookie viene prima cancellata e ricreata dai cookie memorizzati nella raccolta.

L’implementazione della sessione ASP.NET utilizza la proprietà System.Web.HttpResponse.Cookies per memorizzare il suo cookie ASP.NET_SessionId. Inoltre vi è una certa ottimizzazione di base nel modulo dello stato di sessione ASP.NET ( System.Web.SessionState.SessionStateModule ) implementato tramite la proprietà statica denominata s_sessionEverSet che è piuttosto auto-esplicativa. Se memorizzi qualcosa nello stato di sessione nella tua applicazione, questo modulo farà un po ‘più di lavoro per ogni richiesta.


Torna al nostro problema di accesso

Con tutti questi pezzi i tuoi scenari possono essere spiegati.

Caso 1: la sessione non è mai stata impostata

La proprietà System.Web.SessionState.SessionStateModule , s_sessionEverSet è false. Nessun ID di sessione viene generato dal modulo di stato sessione e lo stato di raccolta System.Web.HttpResponse.Cookies non viene rilevato come modificato . In questo caso i cookie OWIN vengono inviati correttamente al browser e il login funziona.

Caso 2: la sessione è stata utilizzata da qualche parte nell’applicazione, ma non prima che l’utente tenti di autenticarsi

La proprietà System.Web.SessionState.SessionStateModule , s_sessionEverSet è true. Gli ID sessione vengono generati da SessionStateModule , ASP.NET_SessionId viene aggiunto alla raccolta System.Web.HttpResponse.Cookies ma viene rimosso in un secondo momento nel corso della durata della richiesta poiché la sessione dell’utente è in effetti vuota. In questo caso, lo stato della raccolta System.Web.HttpResponse.Cookies viene rilevato come modificato e l’intestazione Set-Cookie viene prima cancellata prima che i cookie vengano serializzati sul valore dell’intestazione.

In questo caso i cookie di risposta OWIN sono “persi” e l’utente non è autenticato e viene reindirizzato alla pagina di accesso.

Caso 3: la sessione viene utilizzata prima che l’utente tenti di autenticarsi

La proprietà System.Web.SessionState.SessionStateModule , s_sessionEverSet è true. Gli ID sessione vengono generati da SessionStateModule , ASP.NET_SessionId viene aggiunto a System.Web.HttpResponse.Cookies . A causa dell’ottimizzazione interna in System.Web.HttpCookieCollection e System.Web.HttpResponse.GenerateResponseHeadersForCookies () l’ intestazione Set-Cookie NON viene prima cancellata ma aggiornata.

In questo caso, i cookie di autenticazione OWIN e il cookie ASP.NET_SessionId vengono inviati in risposta e il login funziona.


Problema più generale con i cookie

Come puoi vedere il problema è più generale e non limitato alla sessione ASP.NET. Se stai ospitando OWIN tramite Microsoft.Owin.Host.SystemWeb e tu / qualcosa stai utilizzando direttamente la raccolta System.Web.HttpResponse.Cookies sei a rischio.

Ad esempio questo funziona e entrambi i cookie sono correttamente inviati al browser …

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

Ma questo no e OwinCookie è “perso” …

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

Entrambi testati da VS2013, IISExpress e modello di progetto MVC predefinito.

A partire dalla grande analisi di @TomasDolezal, ho dato un’occhiata sia alla sorgente di Owin che a quella di System.Web.

Il problema è che System.Web ha la propria fonte principale di informazioni sui cookie e che non è l’intestazione Set-Cookie. Owin conosce solo l’intestazione Set-Cookie. Una soluzione alternativa è assicurarsi che tutti i cookie impostati da Owin siano impostati anche nella raccolta HttpContext.Current.Response.Cookies .

Ho creato un piccolo middleware ( source , nuget ) che fa esattamente questo, che è destinato ad essere collocato immediatamente sopra la registrazione del middleware dei cookie.

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

In breve, il gestore di cookie .NET vincerà il gestore di cookie OWIN e sovrascriverà i cookie impostati sul livello OWIN . La correzione consiste nell’utilizzare la class SystemWebCookieManager, fornita come soluzione sul progetto Katana qui . È necessario utilizzare questa class o una di esse simile, che obbligherà OWIN a utilizzare il gestore di cookie .NET, quindi non ci sono incongruenze :

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

Nell’avvio dell’applicazione, è sufficiente assegnarlo quando si creano le dipendenze OWIN:

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

Una risposta simile è stata fornita qui ma non include tutto il codice richiesto per risolvere il problema, quindi vedo la necessità di aggiungerlo qui perché il link esterno al Katana Project potrebbe andare giù e questo dovrebbe essere completamente descritto come soluzione anche qui.

Il team di Katana ha risposto al problema sollevato da Tomas Dolezar e ha pubblicato la documentazione sui workaround :

I metodi alternativi si dividono in due categorie. Uno è quello di riconfigurare System.Web in modo da evitare l’uso della raccolta Response.Cookies e sovrascrivere i cookie OWIN. L’altro approccio consiste nel riconfigurare i componenti OWIN interessati in modo che scrivano i cookie direttamente nella raccolta Response.Cookies di System.Web.

  • Assicurarsi che la sessione sia stabilita prima dell’autenticazione: il conflitto tra i cookie System.Web e Katana è per richiesta, quindi potrebbe essere ansible per l’applicazione stabilire la sessione su alcune richieste prima del stream di autenticazione. Questo dovrebbe essere facile da fare quando l’utente arriva per la prima volta, ma potrebbe essere più difficile garantire in un secondo momento quando i cookie di sessione o di autenticazione scadono e / o devono essere aggiornati.
  • Disabilitare SessionStateModule – Se l’applicazione non si basa sulle informazioni di sessione, ma il modulo di sessione sta ancora impostando un cookie che causa il conflitto di cui sopra, è ansible prendere in considerazione la disabilitazione del modulo dello stato di sessione.
  • Riconfigurare CookieAuthenticationMiddleware per scrivere direttamente nella raccolta di cookie di System.Web.
 app.UseCookieAuthentication(new CookieAuthenticationOptions { // ... CookieManager = new SystemWebCookieManager() }); 

Vedere l’implementazione di SystemWebCookieManager dalla documentazione (link sopra)

Maggiori informazioni qui

modificare

Di seguito i passaggi che abbiamo preso per risolvere il problema. Entrambi 1. e 2. hanno risolto il problema anche separatamente, ma abbiamo deciso di applicare entrambi nel caso in cui:

1. Usa SystemWebCookieManager

2. Imposta la variabile di sessione:

 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 a margine: il metodo Initialize di cui sopra è il posto logico per la correzione perché base.Initialize rende disponibile Session. Tuttavia, la correzione potrebbe essere applicata anche in seguito, poiché in OpenId è prima richiesta anonimo, quindi reindirizza al provider OpenId e quindi torna indietro all’app. I problemi si verificherebbero dopo il reindirizzamento verso l’app mentre la correzione imposta la variabile di sessione già durante la prima richiesta anonima, risolvendo così il problema prima che si verifichino nuovamente eventuali reindirizzamenti)

Modifica 2

Copia-incolla dal progetto Katana 2016-05-14:

Aggiungi questo:

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

…e questo:

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

Le risposte sono già state fornite, ma in owin 3.1.0 è disponibile una class SystemWebChunkingCookieManager.

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 si stanno impostando i cookie nel middleware OWIN, quindi utilizzare OnSendingHeaders sembra aggirare il problema.

Ad esempio, verrà impostato il codice sottostante owinResponseCookie2 , anche se owinResponseCookie1 non è:

 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 soluzione di codice one-line più veloce:

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

Basta aggiungere questa riga prima del metodo CreateIdentity:

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

Ho avuto lo stesso sintomo dell’intestazione Set-Cookie non inviata, ma nessuna di queste risposte mi ha aiutato. Tutto ha funzionato sul mio computer locale ma, una volta distribuito in produzione, le intestazioni del set-cookie non sarebbero mai state impostate.

Si scopre che si trattava di una combinazione di utilizzo di CookieAuthenticationMiddleware personalizzato con WebApi insieme al supporto per la compressione WebApi

Fortunatamente stavo usando ELMAH nel mio progetto che mi ha permesso di registrare questa eccezione:

System.Web.HttpException Server non può aggiungere l’intestazione dopo che le intestazioni HTTP sono state inviate.

Il che mi ha portato a questo numero di GitHub

Fondamentalmente, se hai una configurazione strana come la mia, vorrai disabilitare la compressione per i tuoi controller / metodi WebApi che impostano i cookie, o prova OwinServerCompressionHandler .