dotnet-cas-client: Ticket Caching Failing With Visual Studio 2017

The TL;DR is that when running a CAS-enabled application in Visual Studio 2017 (IIS Express), the .NET cache provider (System.Web.Caching.Cache) is immediately expiring the new service tickets as soon as they are added to the cache. Thus, no service ticket is ever able to be validated. Below is how I arrived at this conclusion.

Recently my development computer at work bit the dust, prompting me to reformat and re-install Windows 10. I took that opportunity to also upgrade to Visual Studio 2017 Enterprise (from 2015 Enterprise). When I began working on one of my CAS-enabled applications in VS2017, I immediately got the problem of a redirect cycle between my application and the CAS server.

First thing I checked was that it wasn’t another timing problem with the tolerance level for the SAML tickets, which it wasn’t. No matter what value I set for ticketTimeTolerance, the redirects still occurred. I debugged about all I could think of in my application with no indication of why the SAML ticket validation was failing and thus redirecting back to the CAS server. Even turning on the tracing per the instructions in this project’s README didn’t shine light on the source of the problem.

I finally broke down and added the DotNetCasClient project to my solution so that I could step through the code as the process was happening. Eventually I noticed that the service ticket was being added to the cache, but in a subsequent call to retrieve the ticket from the cache, null was returned. I added some code for debugging to InsertTicket(). I was able to see that, immediately after inserting the ticket into the cache the cache item count increased by one. However, after a few debugging steps, the cache count decreased by one. I read in the comments for RemoveExpiredTickets() that the cache will automatically remove any expired items, so this got me thinking that the problem was with the expiration date for the cache item being inserted. By pure luck I was able to notice that the dates for the forms authentication ticket were using UTC while the expiration date for the cache item was in local time. Furthermore, I noticed the comments for the Insert() method of System.Web.Caching.Cache recommend to always use UTC dates as issues can occur with daylight savings time. The particular problem I am having wasn’t related to DST, but on a whim I changed the insert statement from:

HttpContext.Current.Cache.Insert(GetTicketKey(casAuthenticationTicket.ServiceTicket), casAuthenticationTicket, null, expiration, Cache.NoSlidingExpiration);

to:

HttpContext.Current.Cache.Insert(GetTicketKey(casAuthenticationTicket.ServiceTicket), casAuthenticationTicket, null, expiration.ToUniversalTime(), Cache.NoSlidingExpiration);

Immediately my application worked again. To double-check that the fix was correct I have created another dummy web application that consumes CAS in Visual Studio 2017. Using the local date/time, the redirect loop occurs. Modify it to use UTC date/time, the application works as expected.

I created the same dummy application in a copy of Visual Studio 2015 I have, targeting the same version of the .NET framework (v4.5.2). Using the same configuration settings for the client in web.config, I did the same tests, except this time the caching of the service tickets works regardless of using local or UTC time for expiration. As best as I can tell, the only difference between the projects is Visual Studio 2015 versus 2017. They target the same framework version, have all the same NuGet packages/versions, and use the same web.config settings.

Using those same dummy projects (one in 2015, the other in 2017) I basically copied the code from ContainsTicket() to list the contents of the cache on each page load. Using a special query string parameter, I could instruct the page load code to add a simple string value as an object to the cache with a +1 minute expiration date. In VS2017, using the local date for expiration, the string would be gone from the cache on the next load, even though the second request was only a few seconds after adding the object to the cache. Using the UTC date, the string would stay in the cache for a minute, then be removed. Switching to VS2015 yielded the expected result that it did not matter which form of the date/time was used for expiration. The string value would stay in cache for 1 minute, then expire.

According to the MSDN Documentation for the Insert() method, the recommendation for the absoluteExpiration parameter value is for it to be a UTC date/time, not a local one. It is laughable, though, that the examples on the same page use DateTime.Now, against their own recommendation. In any case, I think it would be a good thing to change the expiration to a UTC date/time regardless of it being the absolute root cause of the problem I am experiencing.

I believe the change would need to be made at the point where the expiration date is set, which is when the forms authentication ticket is being created in CreateFormsAuthenticationTicket() of CasAuthentication.cs. The fromDate and toDate variables can be set as UTC dates, and the client has no problem in VS2017. I will create a pull request soon with the suggested changes along with documentation as to why UTC dates are needed. In the meantime, if anyone wants to weigh in on why this is happening between VS2015 (and earlier) and VS2017, it would be appreciated!

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 3
  • Comments: 38 (20 by maintainers)

Commits related to this issue

Most upvoted comments

Heads up everyone!

We now have a pre-release NuGet package feed for alpha/beta/unstable versions of the DotNetCasClient package. It can be found here: https://www.myget.org/gallery/dotnetcasclient-prerelease

Can you guys/gals please connect to the new feed and test the 1.1.0-beta0001 version of the package to see if this resolves the problem for you? Report back if you have any problems or if the issue was resolved for you.

There were some other changes in this pre-release version, those can be found in the ReleaseNotes.md file found in the release/1.1.0 branch right now.

Thanks!

@hokiecoder @danransom @webMan1 @serac @sbubaron @nicswan @scottt732 @mmoayyed

As a note, this issue manifested for me because .NET 4.7 was pushed as a critical update last week so was automatically added to production servers (yay!). So I imagine this will quickly become an issue for others as well. Thanks to @hokiecoder / @danransom for the fix. I was able to roll my own version and fix the issue for at least one of my apps.

But IMHO, we should switch out to the System.Runtime.Caching provider for the .NET 4.x targets (it’s not in .NET 2/3.x because it was part of Enterprise Library at that point.)

I’m suggesting the dual compilation targets because if you keep it all a single target, then you can’t add new features when you need them because you are being held back by the lowest framework you support.

We have plans to build a target for .NET Core and as a result, the nuget package will have more than one assembly in it. Otherwise it’s just not possible to have one assembly that fits .NET and .NET Core.

So here are my thoughts for this issue after discussing with my team.

  • This bug, problem if you will, is introduced when .NET 4.7 is installed on the machine that this code (your web application) runs on.
  • .NET 2.0 and 3.x applications are unaffected. The only impact is on .NET 4.x applications.

The following thoughts are in preparation of planning for the future of this project…

  • The nuget package will have two assemblies in it for this bug fix.
  • When you install the nuget package, if your application targets the .NET 2.0/3.x framework, then you get the assembly compiled for that version of the framework and will contain the same code as today to maintain backwards compatibility.
  • If you install the nuget package in an application that targets the .NET 4.x framework, you get this assembly compiled for that version of the framework. Herein lies the reason: I think it’s easiest to swap out the cache provider with System.Runtime.Caching and not muck with the expiration date.
  • This single nuget package, multiple targeted assemblies is a common thing in the .NET landscape. This can be seen in many packages, including the most downloaded which is JSON.NET.
  • This way there is no need to add/change web.config settings and things will just work out of the box without developer intervention. Think of it as an “under the hood” change.

One of our future goals would be to introduce the concept of a plugable cache provider interface where you could use the default standard in-memory cache provider, the two described above, or a third party of your choosing. One use case would be to allow for a distributed cache provider such as memcached or redis. This will make it easier to move CAS enabled .NET applications to a load balanced environment, especially if “sticky sessions” are not enough.