If you have developed a .NET 3.5 or .NET 4.0 claims aware web application using Windows Identity Foundation (WIF) 1.0, you may run into the following exception when attempting to navigate to the web site, especially if you’ve hosted the website behind a load balancer:

 

System.InvalidOperationException: ID1073: A CryptographicException occurred when attempting to decrypt the cookie using the ProtectedData API (see inner exception for details). If you are using IIS 7.5, this could be due to the loadUserProfile setting on the Application Pool being set to false. 

Stack Trace Info: [CryptographicException: Key not valid for use in specified state.]
at System.Security.Cryptography.ProtectedData.Unprotect(Byte[] encryptedData, Byte[] optionalEntropy, DataProtectionScope scope)
at Microsoft.IdentityModel.Web.ProtectedDataCookieTransform.Decode(Byte[] encoded)

[InvalidOperationException: ID1073: A CryptographicException occurred when attempting to decrypt the cookie using the ProtectedData API (see inner exception for details). If you are using IIS 7.5, this could be due to the loadUserProfile setting on the Application Pool being set to false. ]
at Microsoft.IdentityModel.Web.ProtectedDataCookieTransform.Decode(Byte[] encoded)
at Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ApplyTransforms(Byte[] cookie, Boolean outbound)
at Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ReadToken(XmlReader reader, SecurityTokenResolver tokenResolver)
at Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ReadToken(Byte[] token, SecurityTokenResolver tokenResolver)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.ReadSessionTokenFromCookie(Byte[] sessionCookie)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.TryReadSessionTokenFromCookie(SessionSecurityToken& sessionToken)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.OnAuthenticateRequest(Object sender, EventArgs eventArgs)
at Microsoft.Crm.Authentication.Claims.CrmSessionAuthenticationManager.OnAuthenticateRequest(Object sender, EventArgs args)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

 

When you authenticate against a claims aware web application, using WS-Federation (think browser redirects), WIF will generate a browser session cookie so that you can continue to browse to resources within the same application without having to re-authenticate with the identity provider. The session cookie is secured using DPAPI, which means that the server that secured that session cookie, is the only machine that will be able to decrypt the cookie to pull out the contents, specifically the SAML assertion containing your claims.

If you see this error message and you are hosting your claims aware web application on a standalone machine, then the error details properly indicates that you may just need to enable loading of the user profile, which is a requirement for using DPAPI. However if you see this error message occur because you have your claims aware application hosted behind a load balancer then you will have to take other steps to resolve the issue.

If your claims aware ASP.NET 3.5 or 4.0 application is hosted behind a load balancer, then it is possible that your user navigated to SeverA, got the session cookie protected by DPAPI on ServerA, but as they continue to browse around the site the load balancer redirects the request to execute on ServerB. When this happens, ServerB does not have a matching machine key, so it is unable to decrypt the session cookie and throws the above error. Here are three ways in which you can resolve this issue.

Workaround #1 – Use sticky sessions

One simple way to resolve this issue is to simply configure your load balancer to use sticky sessions. When you enable sticky sessions then your load balancer will always redirect the user to the same backend server to handle the requests. Using this configuration, the server that encrypts the session cookie using DPAPI will be the only server that will be responsible for decrypting that cookie. One tip to look out for is to make sure sticky sessions are enabled across both HTTP and HTTPS protocols.

Workaround #2 – Use X509 Certificate to secure sessions cookies

Another popular solution is to reconfigure WIF to use an X509 certificate to protect the session cookie instead of using DPAPI. When you do this, then you just have to make sure each server behind your load balancer has the same certificate installed and WIF is configured to use this certificate when it wants to protect the session cookie before it sends it back to the client browser. For this workaround you will replace the default SessionSecurityTokenHandler cookie transform list with a list that includes the RsaEncryptionCookieTransform. You will specify which certificate to use in the RsaEncryptionCookieTransform constructor.

There are a couple of ways to accomplish this. The first way would be to simply modify the SessionSecurityTokenHandler used by tapping into the FederatedAuthentication.OnServiceConfigurationCreated event. Reference the following links for details on how to add this to your global.asax page to support this cookie transform:

Another approach to this workaround is to simply create your own SessionSecurityTokenHandler-derived class that will use the RSA based cookie transforms. Here is an example class that you could implement:

using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Web;


namespace RsaSessionCookie
{
/// <summary>
/// This class encrypts the session security token using the RSA key
/// of the relying party's service certificate.
/// </summary>
public class RsaEncryptedSessionSecurityTokenHandler : SessionSecurityTokenHandler
{
static List<CookieTransform> _transforms;

static RsaEncryptedSessionSecurityTokenHandler()
{
//
// TODO: Update the subject name
//
X509Certificate2 serviceCertificate = CertificateUtil.GetCertificate( StoreName.My,
StoreLocation.LocalMachine, "CN=YourUniqueCertificateSubjectName" );
_transforms = new List<CookieTransform>()
{
new DeflateCookieTransform(),
new RsaEncryptionCookieTransform( serviceCertificate ),
new RsaSignatureCookieTransform( serviceCertificate ),
};
}

public RsaEncryptedSessionSecurityTokenHandler()
: base(_transforms.AsReadOnly())
{
}
}

/// <summary>
/// A utility class which helps to retrieve an x509 certificate
/// </summary>
public class CertificateUtil
{
/// <summary>
/// Gets an X.509 certificate given the name, store location and the subject distinguished name of the X.509 certificate.
/// </summary>
/// <param name="name">Specifies the name of the X.509 certificate to open.</param>
/// <param name="location">Specifies the location of the X.509 certificate store.</param>
/// <param name="subjectName">Subject distinguished name of the certificate to return.</param>
/// <returns>The specific X.509 certificate.</returns>
public static X509Certificate2 GetCertificate( StoreName name, StoreLocation location, string subjectName )
{
X509Store store = new X509Store( name, location );
X509Certificate2Collection certificates = null;
store.Open( OpenFlags.ReadOnly );

try
{
X509Certificate2 result = null;

//
// Every time we call store.Certificates property, a new collection will be returned.
//
certificates = store.Certificates;

for ( int i = 0; i < certificates.Count; i++ )
{
X509Certificate2 cert = certificates[i];

if ( cert.SubjectName.Name.ToLower() == subjectName.ToLower() )
{
if ( result != null )
throw new ApplicationException( string.Format( "There is more than one certificate found for subject Name {0}", subjectName ) );
result = new X509Certificate2( cert );
}
}

if ( result == null )
{ throw new ApplicationException( string.Format( "No certificate was found for subject Name {0}", subjectName ) ); }
return result;
}
finally
{
if ( certificates != null )
{
for ( int i = 0; i < certificates.Count; i++ )
{
X509Certificate2 cert = certificates[i];
cert.Reset();
}
}
store.Close();
}
}
}
}

 

Compile this code into an assembly, sign the assembly, and deploy it to the global assembly cache. Then back in your claims aware web application you can use WIF configuration to use this custom SessionSecurityTokenHandler class, which will use RSA cookie transforms instead of the default DPAPI. Here is how you can configure WIF to use the example class above from a dll that was named RsaSessionCookieHandler.dll:

<microsoft.identityModel>
<service>

<securityTokenHandlers>
<!-- Remove and replace the default SessionSecurityTokenHandler with your own -->
<remove type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add type="RsaSessionCookie.RsaEncryptedSessionSecurityTokenHandler, RsaSessionCookieHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=enter_public_key_token_for_signed_dll..." />
</securityTokenHandlers>
</service>
</microsoft.identityModel>

 

Now when navigating to your claims aware web application, WIF will use your custom SessionSecurityTokenHandler which will use an X509 Certificate to secure the session cookie instead of DPAPI. If your users are redirected to a different server behind the firewall, then if that server has the same certificate then it will be able to decrypt the cookie and pull out the claims information for your user session.

Workaround #3 – Use DPAPI with matching machine keys

One last workaround to this scenario is to continue to use DPAPI, but you will configure IIS to use matching machine keys between all the servers behind your load balancer. The WIF 1.0 SDK has a sample that discusses this workaround. Once you install the SDK then review the sample solution and readme.htm file found in the directory C:\Program Files (x86)\Windows Identity Foundation SDK\v4.0\Samples\Quick Start\Web Farm. This solution will use the MachineKeyProtectionTransform() in another custom SessionSecurityTokenHandler class.

Cheers,
Todd Foust