Alternative to hardcoding usernames and passwords - asp.net-mvc

I am writing an MVC4 app using C# Razor and all that great stuff. It has full login and password along with an extra 2 questions required before the user can login.
Before I enabled the login feature several months back, it was a dream, I just start the app on the page I was interested in, and it loads away allowing me to instantly see my results.
Occasionally I would start on the wrong page and god forbid I might have to do an extra click or two to get to the right page. I considered this bad enough.
Now that I have enabled the login and since done several modifications to the code, it has been through the testing department, and out to the big bad world. I am not allowed to disable it again.
My alternative was to essentially hardcode the testing username and password and pin with the other default values required and then remove them upon release. Thank god for testing...I actually left them hardcoded once. This was enough to teach me the lesson never to do it again.
However, I start my application literally without exaggeration possibly hundreds (although it feels like thousands) of times a day and every single time I have to fill in this dreaded login form. this is extremely unproductive and really uses up a lot of my time. It actively encourages me to do bad programming habits of changing several things at once and then testing. A road I don't want to start down.
My question: Is there an alternative to this hardcoding practice that will let me get back my productivity?
Please don't suggest allowing the browser to remember the details. I tried, the problem is that my app must be cross browser and platform compatible which means it not only has to run in windows browsers (which has enough variation in themselves), but also on tablets, phones and even lunix and macs. So relying on the browser to remember the details is simply not an option.
Any suggestions.
Based on suggestions below I decided to explore the web/user.config route, here are my attempts so far which are not working.
Model:
public class LogOnModel
{
[Required]
[Display(Name = "User name")]
[StringLength(255, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 8)]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
[StringLength(255, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
public string Password { get; set; }
}
Controllers:
public ActionResult Login()
{
LogOnModel model = new LogOnModel();
model.UserName = ConfigurationManager.AppSettings["UserName"];
model.Password = ConfigurationManager.AppSettings["Password"];
return View("Login");
}
[HttpPost]
public ActionResult Login(LogOnModel model, string returnUrl)
{
model.UserName = ConfigurationManager.AppSettings["UserName"];
model.Password = ConfigurationManager.AppSettings["Password"];
//I am aware that this will presently overwrite the incoming model if it exists.
//I am just trying to get something work here and I will surround it with an if afterward.
if (ModelState.IsValid)
{
... Other code here
}
... Other code here (there are a variety of returns this is a long method with lots of checks
}
View:
#model YeatsClinical.PatientPortal.Web.Models.LogOnModel
...Lots of other code...
#Html.TextBoxFor(x=>x.UserName, new { #class = "m-wrap placeholder-no-fix", #placeholder="Username"})
...Lots of other code...
I didn't bother trying the password yet, I just wanted to get one thing working first.
Web.Config:
<appSettings file="user.config">
User.Config
<appSettings>
<add key="UserName" value ="someone#somewhere.com"/>
<add key="Password" value ="password"/>
</appSettings>
There are no longer any web/user.config errors or any build errors. It just loads a nicely formatted textbox with the placeholder text as before. So where am I going wrong. Thanks again everyone.

Put your login details in your local web.config file. Your code will look for the config values, and if there, will auto-fill them in for you. You can leave this code in when you deploy - since those values are not in the config file in production, you won't have to worry about it running there.
Or you can put them in a separate config file, like user.config, and reference that in your main config. Your user.config file will never be published. Just make sure your deployment practice doesn't bring this file in.
Web.config:
<configuration>
<appSettings file="user.config">
<!-- put your normal config values here -->
</appSettings>
</configuration>
user.config (only your local machine - not in source control, and not on the server)
<appSettings>
<add key="defaultUser" value ="someUser"/>
<add key="defaultPassword" value ="somePassword"/>
</appSettings>
Another alternative might be to use conditional compilation, assuming you're in debug mode locally and release in release mode.
#if DEBUG
PopulateTheUserCredentials();
#endif

Related

MachineKeyDataProtector - Invalid link when confirmation email sent through background job

I've been pulling my hair out over this. Anytime a user registration email is sent out via my windows service (background task), I get an "Invalid link".
My setup
I'm using Hangfire as a windows service on our development server. This is where the problematic GenerateEmailConfirmationToken call is happening. It's in a completely different context, outside of the ASP.NET pipeline. So I have setup machineKey values to correspond with that in the web.config of the MVC application:
In the app.config of the Windows Service Console project, which transforms to MyApp.exe.config, I have a machineKey element
In the MVC 5 project - I have a machineKey element that matches the MyApp.exe.config machineKey element.
I've verified that BOTH of these have the same machine key element data.
The Problem
When I generate a user using the ASP.NET MVC context and pipeline (IE without going through the Hangfire Background job processing), the link works fine.
When I use the background job processor, I always get invalid link. I'm all out of ideas here.
Why is this happening? Is it because the token is being generated in a different thread? How do I get around this?
Relevant code for the various projects
IoC Bootstrapping
Gets called by both applications (Windows Service and MVC Web App)
container.Register<IUserTokenProvider<AppUser, int>>(() => DataProtector.TokenProvider, defaultAppLifeStyle);
DataProtector.cs
public class DataProtector
{
public static IDataProtectionProvider DataProtectionProvider { get; set; }
public static DataProtectorTokenProvider<AppUser, int> TokenProvider { get; set; }
static DataProtector()
{
DataProtectionProvider = new MachineKeyProtectionProvider();
TokenProvider = new DataProtectorTokenProvider<AppUser, int>(DataProtectionProvider.Create("Confirmation", "ResetPassword"));
}
}
Things I've Tried
Using a DpapiDataProtectionProvider
Custom MachineKeyProtectionProvider from Generating reset password token does not work in Azure Website
The MachineKeyProtectionProvider.cs code is exactly as the linked post above.
I've also tried other purposes like "YourMom" and "AllYourTokensAreBelongToMe" to no avail. Single purposes, multiple purposes - it doesn't matter - none work.
I'm also calling HttpUtility.UrlEncode(code) on the code that gets generated in both places (Controller and Background Job).
Solution
igor got it right, except it was not a code issue. It was because of a rogue service picking up the job, which had a different machine key. I had been staring at the problem so long that I did not see a second service running.
As I understand your problem there are 2 possible places where failure could occur.
1. MachineKey
It could be that the MachineKey itself is not producing a consistent value between your 2 applications. This can happen if your machineKey in the .config file is not the same in both applications (I did read that you checked it but a simple type-o, added space, added to the wrong parent element, etc. could lead to this behavior.). This can be easily tested to rule it out as a point of failure. Also the behavior might be different depending on the referenced .net framework, MachineKey.Protect
The configuration settings that are required for the MachineKeyCompatibilityMode.Framework45 option are required for this method even if the MachineKeySection.CompatibilityMode property is not set to the Framework45 option.
I created a random key pair for testing and using this key I generated a test value I assigned to variable validValue below in the code. If you copy/paste the following section into your web.config and app.config the Unprotect of that keyvalue will work.
web.config / app.config
<system.web>
<httpRuntime targetFramework="4.6.1"/>
<machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209" validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>
Service Application Test
class Program
{
static void Main(string[] args)
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
}
}
Mvc Controller (or Web Api controller) Test
public class WebTestController : Controller
{
// GET: WebTest
public ActionResult Index()
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
return View(unprotected2);
}
}
Common Code
using System;
using System.Linq;
using System.Text;
using System.Web.Security;
namespace Common
{
public class MachineWrapper
{
public static string Protect()
{
var testData = "SomeTestString";
return BytesToString(MachineKey.Protect(System.Text.Encoding.UTF8.GetBytes(testData), "PasswordSafe"));
}
public static string Unprotect(string data)
{
var bytes = StringToBytes(data);
var result = MachineKey.Unprotect(bytes, "PasswordSafe");
return System.Text.Encoding.UTF8.GetString(result);
}
public static byte[] StringToBytes(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
public static string BytesToString(byte[] bytes)
{
var hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
hex.AppendFormat("{0:x2}", b);
return hex.ToString().ToUpper();
}
}
}
If this passes both Console and the Web Application will get the same value and not throw a CryptographicException message Error occurred during a cryptographic operation. If you want to test with your own keys just run Protect from the common MachineWrapper class and record the value and re-execute for both apps.
2. UserManager uses Wrong Type
I would start with the previous section BUT the other failure point is that your custom machine key provider is not being used by the Microsoft.AspNet.Identity.UserManager. So here are some questions/action items that can help you figure out why this is happening:
Is container.Register the Unity IoC framework or are you using another framework?
Are you sure that your Di framework is also injecting that instance in the Microsoft.AspNet.Identity.UserManager in both the Service application as well as the Web application?
Have put a break point in public byte[] Protect of your MachineKeyDataProtector class to see if this is called in both the Service application as well as the Web application?
From examples I have seen so far (including the one you posted with the custom MachineKey solution) you need to manually bootstrap the type during application startup but then again I have not ever tried to hook into the Identity framework to replace this component using DI.
If you look at the default Visual Studio template code that is provided when you create a new MVC application the code file App_Start\IdentityConfig.cs would be the place to add this new provider.
Method:
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
Replace
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
With this
var provider = new MachineKeyProtectionProvider();
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("ResetPasswordPurpose"));
And this has to be configured for both applications if you are not using a common library where this is configured.

The anti-forgery token could not be decrypted

I have a form:
#using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl })) {
#Html.AntiForgeryToken()
#Html.ValidationSummary()...
and action:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl, string City)
{
}
occasionally (once a week), I get the error:
The anti-forgery token could not be decrypted. If this application is
hosted by a Web Farm or cluster, ensure that all machines are running
the same version of ASP.NET Web Pages and that the configuration
specifies explicit encryption and validation keys. AutoGenerate cannot
be used in a cluster.
i try add to webconfig:
<machineKey validationKey="AutoGenerate,IsolateApps"
decryptionKey="AutoGenerate,IsolateApps" />
but the error still appears occasionally
I noticed this error occurs, for example when a person came from one computer and then trying another computer
Or sometimes an auto value set with incorrect data type like bool to integer to the form field by any jQuery code please also check it.
I just received this error as well and, in my case, it was caused by the anti-forgery token being applied twice in the same form. The second instance was coming from a partial view so wasn't immediately obvious.
validationKey="AutoGenerate"
This tells ASP.NET to generate a new encryption key for use in encrypting things like authentication tickets and antiforgery tokens every time the application starts up. If you received a request that used a different key (prior to a restart for instance) to encrypt items of the request (e.g. authenication cookies) that this exception can occur.
If you move away from "AutoGenerate" and specify it (the encryption key) specifically, requests that depend on that key to be decrypted correctly and validation will work from app restart to restart. For example:
<machineKey
validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75D7
AD972A119482D15A4127461DB1DC347C1A63AE5F1CCFAACFF1B72A7F0A281B"
decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719F"
validation="SHA1"
decryption="AES"
/>
You can read to your heart's content at MSDN page: How To: Configure MachineKey in ASP.NET
Just generate <machineKey .../> tag from a link for your framework version and insert into <system.web><system.web/> in Web.config if it does not exist.
Hope this helps.
If you get here from google for your own developer machine showing this error, try to clear cookies in the browser. Clear Browser cookies worked for me.
in asp.net Core you should set Data Protection system.I test in Asp.Net Core 2.1 or higher.
there are multi way to do this and you can find more information at Configure Data Protection and Replace the ASP.NET machineKey in ASP.NET Core and key storage providers.
first way: Local file (easy implementation)
startup.cs content:
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// .... Add your services like :
// services.AddControllersWithViews();
// services.AddRazorPages();
// ----- finally Add this DataProtection -----
var keysFolder = Path.Combine(WebHostEnvironment.ContentRootPath, "temp-keys");
services.AddDataProtection()
.SetApplicationName("Your_Project_Name")
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
}
}
second way: save to db
The Microsoft.AspNetCore.DataProtection.EntityFrameworkCore NuGet
package must be added to the project file
Add MyKeysConnection ConnectionString to your projects
ConnectionStrings in appsettings.json > ConnectionStrings >
MyKeysConnection.
Add MyKeysContext class to your project.
MyKeysContext.cs content:
public class MyKeysContext : DbContext, IDataProtectionKeyContext
{
// A recommended constructor overload when using EF Core
// with dependency injection.
public MyKeysContext(DbContextOptions<MyKeysContext> options)
: base(options) { }
// This maps to the table that stores keys.
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
startup.cs content:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ----- Add this DataProtection -----
// Add a DbContext to store your Database Keys
services.AddDbContext<MyKeysContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyKeysConnection")));
// using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.PersistKeysToDbContext<MyKeysContext>();
// .... Add your services like :
// services.AddControllersWithViews();
// services.AddRazorPages();
}
}
If you use Kubernetes and have more than one pod for your app this will most likely cause the request validation to fail because the pod that generates the RequestValidationToken is not necessarily the pod that will validate the token when POSTing back to your application. The fix should be to configure your nginx-controller or whatever ingress resource you are using and tell it to load balance so that each client uses one pod for all communication.
Update: I managed to fix it by adding the following annotations to my ingress:
https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/
Name Description Values
nginx.ingress.kubernetes.io/affinity Sets the affinity type string (in NGINX only cookie is possible
nginx.ingress.kubernetes.io/session-cookie-name Name of the cookie that will be used string (default to INGRESSCOOKIE)
nginx.ingress.kubernetes.io/session-cookie-hash Type of hash that will be used in cookie value sha1/md5/index
I ran into this issue in an area of code where I had a view calling a partial view, however, instead of returning a partial view, I was returning a view.
I changed:
return View(index);
to
return PartialView(index);
in my control and that fixed my problem.
I got this error on .NET Core 2.1. I fixed it by adding the Data Protection service in Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection();
....
}
you are calling more than one the #Html.AntiForgeryToken() in your view
I get this error when the page is old ('stale'). A refresh of the token via a page reload resolves my problem. There seems to be some timeout period.
I found a very interesting workaround for this problem, at least in my case. My view was dynamically loading partial views with forms in a div using ajax, all within another form. the master form submits no problem, and one of the partials works but the other doesn't. The ONLY difference between the partial views was at the end of the one that was working was an empty script tag
<script type="text/javascript">
</script>
I removed it and sure enough I got the error. I added an empty script tag to the other partial view and dog gone it, it works! I know it's not the cleanest... but as far as speed and overhead goes...
I know I'm a little late to the party, but I wanted to add another possible solution to this issue. I ran into the same problem on an MVC application I had. The code did not change for the better part of a year and all of the sudden we started receiving these kinds of error messages from the application.
We didn't have multiple instances of the anti-forgery token being applied to the view twice.
We had the machine key set at the global level to Autogenerate because of STIG requirements.
It was exasperating until I got part of the answer here: https://stackoverflow.com/a/2207535/195350:
If your MachineKey is set to AutoGenerate, then your verification
tokens, etc won't survive an application restart - ASP.NET will
generate a new key when it starts up, and then won't be able to
decrypt the tokens correctly.
The issue was that the private memory limit of the application pool was being exceeded. This caused a recycle and, therefore, invalidated the keys for the tokens included in the form. Increasing the private memory limit for the application pool appears to have resolved the issue.
My fix for this was to get the cookie and token values like this:
AntiForgery.GetTokens(null, out var cookieToken, out var formToken);
For those getting this error on Google AppEngine or Google Cloud Run, you'll need to configure your ASP.NET Core website's Data Protection.
The documentation from the Google team is easy to follow and works.
https://cloud.google.com/appengine/docs/flexible/dotnet/application-security#aspnet_core_data_protection_provider
A general overview from the Microsoft docs can be found here:
https://cloud.google.com/appengine/docs/flexible/dotnet/application-security#aspnet_core_data_protection_provider
Note that you may also find you're having to login over and over, and other quirky stuff going on. This is all because Google Cloud doesn't do sticky sessions like Azure does and you're actually hitting different instances with each request.
Other errors logged, include:
Identity.Application was not authenticated. Failure message: Unprotect ticket failed

Login password encryption with active directory

My MVC5 application usage Active Directory and I am unable to use default .Net provided password encryption as AD doesn't support it.
My Controller is:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(Account user)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(user.UserName, user.Password))
{
var principal = user.GetUserPrincipal(user.UserName, user.Password, user.DomainName);
if (principal != null)
{
FormsAuthentication.SetAuthCookie(user.UserName, user.RememberMe);
var returnUrl = GetRedirectFromLoginUrl();
if (Url.IsLocalUrl(returnUrl))
return Redirect(returnUrl);
else
return RedirectToAction("Index", "Home");
}
else
ModelState.AddModelError("", "User Principal not created.");
}
else
{
ModelState.AddModelError("", "Login data is incorrect!");
}
}
else
ModelState.AddModelError("", "Login data is incorrect!");
return View("Login", user);
}
The login works fine however I have a security issue. I can see my username, password, domain, etc. in clear text when I capture the data using IE9 developer tools (screen below):
__RequestVerificationToken=S-DKCSoudfTYsoBh4fj...&UserName=test&Password=testpassword&DomainName=domainName
Web.Config has this code:
<membership defaultProvider="ADMembership">
<providers>
<clear/>
<add name="ADMembership" type="System.Web.Security.ActiveDirectoryMembershipProvider" connectionStringName="ADConn" attributeMapUsername="sAMAccountName" />
</providers>
</membership>
Please help how to encrypt or hide the password.
Looks like this is the default behaviour in IE developer tools. I tried to login in Google, Microsoft sites and it also shows the password in plain text.
I dont know if this is security issue in IE or not but this is how its behaving.
Steps to view your passwrod:
Open Gmail or Live.com in IE9 (I tried in IE9)
F12 (open Developer Tools) -> Go to Network tab
Click Start Capturing button
Enter username/password (you can enter anything to test)
click Stop Capturing and then go to detailed view
in grid click on POST row and Go to "Request Body" tab
see the code with password as plain text at the last of _RequestVerificationToken......
If anyone finds any better solution, please let me know.
Has this question been answered? I just created a quick, MVC4 web app using VS2012. Set the project properties SSL Enabled to true and launched the project (set the solution root to the assigned ssl:port). And yep, you CAN SEE the password (and user ID) in plain text. So nothing to do with AD - it's either intended or a bug.
NOW FOR THE INTERESTING AND SCARY - and StackOverflow, PLEASE READ.
When I do the same F12 view logging into THIS SITE, I also see my user name and password IN PLAIN TEXT!
So either this is a "feature" of IE or potentially a bug in VS and a concern for all of us in StackOverflow.
Please respond ASAP. And BTW, I tried the same test with my bank log in and nope, you see nothing or encrypted text.

Validation with DataAnnotations in Azure shows wrong Text

We creating a windows azure website in MVC4 and we are using dataannotation to set the display-name and also to validate the input fields. It is a multilanguage page and thus we are unsing Resource-Files to translate.
[Display(ResourceType = typeof(GlobalResource), Name = "LitZip")]
[Required(ErrorMessageResourceType = typeof(GlobalResource), ErrorMessageResourceName = "ErrRequiredZip")]
public string ZIP { get; set; }
Local all works perfect. Uploaded on windows azure all shows fine too but the errors after validation are not translated. When I'm returning the CurrentCulture, it's correctly set to german. We are using the custom tool "PublicResXFileCodeGenerator" to generate the resource files.
Hope anybody can help us with this issue.
thanks in advance!
kind reagrds
Edit:
The culture is set by a filterattribute as follows:
var culture = new CultureInfo("de-de");
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
Edit 2013-05-02:
I'm currently setting the culture in the ActionFilterAttribute. When I set the culture in the web.config-File it all works.
<globalization culture="de-DE" uiCulture="de-DE" />
Nevertheless, I need to be able to change the culture on runtime individual for the users. Maybe the ActionFilterAttribute is the wrong position here? I need to access cookie data..
Add this line to Web.Config file.
<globalization uiCulture="auto:ru-RU" culture="auto:ru-RU" requestEncoding="utf-8" responseEncoding="utf-8"/>

Why is the HttpContext.Cache count always zero?

I set up a few pages with OutputCache profiles and confirmed that they are being cached by using multiple browsers and requests to retrieve the page with a timestamp which matched across all requests. When I try to enumerate the HttpContect.Cache it is always empty.
Any ideas what is going on here or where I should be going for this information instead?
Update:
It's not client cache because multiple browsers are seeing the same response. Here is a bit of code to explain what's happening.
Web.Config caching settings
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<clear/>
<add name="StaticContent" duration="1200" varyByParam="none"/>
<add name="VaryByParam" duration="1200" varyByParam="*"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
...
</system.web>
**Action Method With Caching
[OutputCache(CacheProfile = "StaticContent")]
public ActionResult Index()
{
return View(new CollaborateModel());
}
Code to enumerate the cache, yep it's rough, an this is defined in a controller action method
var sb = new StringBuilder();
foreach (KeyValuePair<string, object> item in HttpContext.Cache)
{
sb.AppendFormat("{0} : {1}<br />", item.Key, item.Value.ToString());
}
ViewData.Add("CacheContents", sb.ToString());
The HttpContext.Cache is where the count is always null, even though the cache seems to be working fine.
That's probably because the page has been cached downstream on the client browser and not on the server.
Instead of using the HttpCache I ended up rolling my own caching model for holding datasets in my Data Access layer. If I was looking up the AD profile of a given username and converting it to a DTO then I just put that profile in a rolling collection of profile DTOs that I would check before polling AD for the information.

Resources