SignalR & Forms Auth Expiration

I ran into a problem today with SignalR. That feels weird to say since SignalR always seems to be one library that just works and does exactly what you want.

This particular issue specifically revolved around a site that had a sliding expiration on its forms authentication ticket in ASP.NET MVC. Essentially the problem was that I could leave the window on this site open and idle indefinitely without having to re-authenticate, when my ticket timeout was set to 20 min. In order to test and reproduce the issue locally on my machine, I modified the ticket timeout to 1 min to efficiently test its behavior after ticket expiration. However, it worked perfectly fine and booted me out of the app just as I had expected it would.

I cracked open the good ol’ Chrome Dev tools to see what requests could be possibly happening behind the scenes and it became quickly obvious that SignalR produces a ping request every 5 min by default (which is configurable). This request was keeping my sliding expiration on forms authentication alive pretty much as long as I had the window open (the sliding expiration was useless in this scenario – only good for the session timeout when the user has actually closed the browser as opposed to leaving it idle). My 1 min testing ticket expiration did not work, since the interval from SignalR is defaulted to 5 minutes. I needed to test with at least a 6 min ticket expiration to reproduce the issue (or reconfigure the ping interval to 30 seconds for SignalR).

With the problem narrowed down and reproducible, next is to figure out how to actually fix this issue. One suggestion I found online was to split your site into two different apps, one that was authenticated and one that was not. This seemed to make a simple application OVERLY complex. I figured there had to be a way to prevent only Signalr requests from updating the sliding expiration. Turns out that is pretty easy to do, and the simplest form of it involves simply removing the cookies from any Signalr requests at the end of the pipeline.

There is a great example of this I found posted on GitHub Issues by @moity (near the bottom of the post) that uses an HttpModule to remove the cookie before request headers are sent out:

  1.  
  2. public class SignalRFormsAuthenticationCleanerModule : IHttpModule
  3. {
  4.     public void Init(HttpApplication application)
  5.     {
  6.         application.PreSendRequestHeaders += OnPreSendRequestHeaders;
  7.     }
  8.  
  9.     private bool ShouldCleanResponse(string path)
  10.     {
  11.         path = path.ToLower();
  12.         var urlsToClean = new string[] { "/signalr/", "<and any others you require>" };
  13.  
  14.         // Check for a Url match
  15.         foreach (var url in urlsToClean)
  16.         {
  17.             var result = path.IndexOf(url, StringComparison.OrdinalIgnoreCase) > -1;
  18.             if (result)
  19.                 return true;
  20.         }
  21.  
  22.         return false;
  23.     }
  24.  
  25.     protected void OnPreSendRequestHeaders(object sender, EventArgs e)
  26.     {
  27.          var httpContext = ((HttpApplication)sender).Context;
  28.          if (ShouldCleanResponse(httpContext.Request.Path))
  29.          {
  30.              // Remove Auth Cookie from response
  31.              httpContext.Response.Cookies.Remove(FormsAuthentication.FormsCookieName);
  32.              return;
  33.          }
  34.     }
  35. }

https://github.com/SignalR/SignalR/issues/2907

Worked like a charm! But keep in mind that you’ll stop receiving SignalR events now once the ticket timeout has occurred. Ideally if you could intercept a 401 on the browser you could simple redirect to your login page.