Workaround fix for 'Validation of viewstate MAC failed' bug in Adxstudio Portals 7.23, 7.24, and 7.25

A bug that's important to be aware of was introduced in Portals 7.23 and it's still present in 7.24 and 7.25. It affects any site where forms are submitted anonymously, and causes an exception. I'll discuss the bug and show code that can be manually added to the MasterPortal project code to fix the bug while we wait for Microsoft to publish a new version of Adxstudio Portals with this bug fix.

When a form is submitted anonymously, an exception similar to the following will be shown when error details are visible or as sent through error notification email functionality:

Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that  configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster. See http://go.microsoft.com/fwlink/?LinkID=314055 for more information.
System.Web.HttpException
   at System.Web.UI.ViewStateException.ThrowError(Exception inner, String persistedState, String errorPageMessage, Boolean macValidationError)
   at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString, Purpose purpose)
   at System.Web.UI.Util.DeserializeWithAssert(IStateFormatter2 formatter, String serializedState, Purpose purpose)
   at System.Web.UI.HiddenFieldPageStatePersister.Load()
   at System.Web.UI.Page.LoadPageStateFromPersistenceMedium()
   at System.Web.UI.Page.LoadAllState()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

System.Web

The root cause is due to code that was added in the MasterPortal codebase in v7.23 in the PortalPage.cs file, in what looks to have been an incomplete change meant to add extra security. The error is triggered by line 6 in the following block of code (the line after the comment I've added):

protected override void OnInit(EventArgs args)
{
  if (Session != null && Session.SessionID != null)
  {
    // this line of code introduces the bug:
    ViewStateUserKey = Session.SessionID;
  }
  base.OnInit(args);
}

Setting the ViewStateUserKey property makes view state validation depend on ASP.NET session, and the issue becomes that unauthenticated users will each have different session IDs but will have the same output cached view state, causing the view state validation to fail because their session doesn’t match the one the output cached view state was originally based upon.

We’ve submitted this as a bug to Microsoft and we have a workaround while waiting for a new build of Portals 7. We believe Portals 8 already has this fix because it isn't reproducible in that version.

The code change is to make the output cache label that's generated in Global.asax.cs include the session ID for authenticated users, and for the ViewStateUserKey in PortalPage.cs to only be set when a user is authenticated.

Here is the updated version of OnInit in PortalPage.cs, which now only assigns the ViewStateUserKey to authenticated users:

protected override void OnInit(EventArgs args)
{
  if (Request.IsAuthenticated && Session != null && Session.SessionID != null)
  {
    ViewStateUserKey = Session.SessionID;
  }
  base.OnInit(args);
}

The changes to Global.asax.cs are more complicated, so we'll go through them one at a time.

Add extra using statements for some new namespaces that are used:

using System.Web.Configuration;
using System.Configuration;

Add a new static variable for accessing session state configuration:

private static readonly SessionStateSection SessionStateConfigurationSection = ConfigurationManager.GetSection("system.web/sessionState") as SessionStateSection;

Add a new static method for retrieving the session ID if the current user is signed in:

private static object GetSessionId(HttpContext context)
{
  if (!context.Request.IsAuthenticated || SessionStateConfigurationSection == null)
  {
    return string.Empty;
  }

  var sessionCookie = context.Request.Cookies[SessionStateConfigurationSection.CookieName];
  return sessionCookie != null ? sessionCookie.Value : string.Empty;
}

Add a new static method to obtain the user identity if the current user is signed in:

private static string GetIdentity(HttpContext context)
{
  return context.User != null ? context.User.Identity.Name : string.Empty;
}

Finally, update the GetVaryByUserString method to generate the correct output cache label that now also takes into account the session ID if someone is signed in:

private static string GetVaryByUserString(HttpContext context)
{
  return string.Format("IsAuthenticated={0},Identity={1},Session={2},DisplayModes={3}",
    context.Request.IsAuthenticated,
    identity,
    GetIdentity(context),
    GetSessionId(context),
    GetVaryByDisplayModesString(context).GetHashCode());
}

Until a new version of Portals 7 is released by Microsoft to address this bug, these code changes will need to be manually made in each project.