June 6, 2014

ACS, WIF, ASP .NET and Microsoft Azure – Fixing the exception: 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

This post is about fixing the exception mentioned in the title that I got in an ASP .NET application after deploying to Azure.

When we configure an ASP.NET application as claims aware application using Azure Access Control Service (ACS), we can easily enable login to our application using Windows Live, Google and Yahoo identity providers.

Windows Identity Foundation (WIF) is a set of classes which is a part of .NET framework enable us to easily integrate claim based identity in .NET application.

Authentication flow in nutshell


To debug this exception, we need a basic idea on WIF's role in authentication flow.

WIF::WSFederationAuthenticationModule redirect the user to ACS when user browse to a page which requires him to be authenticated. ACS shows list of enabled identity providers [IdP] (Live, Google, Yahoo), user selects one of the identity provider for login.

Suppose Google is the selected IdP, user will be redirected to Google login page. Once user enters his credentials Google verifies it, if valid, 'Google token issuer' creates a security token containing user's claims. Google IdP redirects browser to ACS with this security token. Once ACS receives this token it validate that it is issued by IdP (and optionally process it), now ACS returns this token (processed token) to user's browser and it again gets POST-ed to the web application.

The WIF::WSFederationAuthenticationModule [FAM] intercept this POST to the web application, parses the security token to extract the claims and prepare an IClaimPrincipal object from it, which is accessible to the application.

FAM calls into WIF::SessionAuthenticationModule [SAM] module, which creates a 'session token' from this IClaimPrincipal object. SAM also takes care of serializing 'session token' to cookie, write this cookie to HTTP response to client. On subsequent request when client present this cookie SAM takes care of deserializing this cookie to IClaimPrincipal object to be used by the application.

Now, lets understand the exception and fix it


WIF creates this browser session cookie so that user can continue to browse to other pages within the same application without having to re-authenticate with the identity provider for each page visit. For security WIF encrypt the cookie.

To read and write session/cookie tokens, WIF::SAM module  uses an instance of the SessionSecurityTokenHandler class, by default it uses Windows DPAPI (Data Protection API) for encryption and decryption of tokens.
    For Encrypt:
    byte [] encryptedCookie = ProtectedData.Protect( cookie, s_aditionalEntropy, DataProtectionScope.CurrentUser );
    
    For Decrypt:
    byte [] cookie = ProtectedData.Unprotect( encryptedCookie, s_ditionalEntropy, DataProtectionScope.CurrentUser );

Note the 'DataProtectionScope.CurrentUser' argument, this cause the protection APIs to use some key generated using current user's 'UserProfile' for encrypt-decrypt.
In order to be able to use DataProtectionScope.CurrentUser option within an application running in IIS, application pool associated with the application should have 'Load User Profile' flag set to 'true'. If this flag set to false, then application running under IIS will not be able to access the current user profile.



By default this 'Load User Profile' flag is 'true'.

All works fine if we host our application in a single machine with 'Load User Profile' on, which will always on by default.

Problem starts when you deploy your application to machine instances behind a load balancer. When user continue to browse the site (after login) the load balancer redirects some request to execute on machine-1 some on machine-2.

If machine-1 receives the first request after login then WIF uses machine-1's current user key to encrypt the cookie. When the next request goes to machine-2, WIF in machine-2 will fail to decrypt the cookie using machine-2's current user key.

You will get the error:

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.

This exception will be thrown from WIF method System.IdentityModel.Web.ProtectedDataCookieTransform.Decode (Byte[] encoded) 
Which uses DPAPI to decode the cookie, when 'DPAPI' fails to do so this method assumes it’s because of 'loadUserProfile' to set false and ask you to do it with above exception, but we know the reason for failure is not 'loadUserProfile' being set to false but because of using different keys for encryption and decryption.

I will explain two solutions to fix this exception:

Solution-1

One solution to this problem is configure WIF not to use the default 'SessionSecurityTokenHandler' (which uses DPAPI) instead use a custom one which uses a key (for encrypt and decrypt) that is shared across all machines. We can register our own 'SessionSecurityTokenHandler' instance within an event handler hooked to the event  'FederatedAuthentication.FederationConfigurationCreated'.

Add the following line in Application Start method in Global.asax.cs:

protected void Application_Start()
{
     // .. other configurations routes etc..
     FederatedAuthentication.FederationConfigurationCreated += OnFederationConfigurationCreated;
}

Now from 'OnFederationConfigurationCreated' register the 'SessionSecurityTokenHandler' as below:

void OnFederationConfigurationCreated(object sender, FederationConfigurationCreatedEventArgs e)
{
    List<CookieTransform> sessionTransforms =
        new List<CookieTransform>(
            new CookieTransform[] {
                new DeflateCookieTransform(),
                new RsaEncryptionCookieTransform(e.FederationConfiguration.ServiceCertificate),
                new RsaSignatureCookieTransform(e.FederationConfiguration.ServiceCertificate)
            }
        );


    SessionSecurityTokenHandler sessionHandler =
    new SessionSecurityTokenHandler(sessionTransforms.AsReadOnly());
    e.FederationConfiguration.
      IdentityConfiguration.
      SecurityTokenHandlers.AddOrReplace(sessionHandler);
}


You can see that for cookie transform, we use a certificate instance pointed by 'e.FederationConfiguration.ServiceCertificate', this will instruct WIF 
to use certificate for cookie encrypt and decrypt instead of DPAPIs.
Next step is to install a certificate in all machines: In Windows Azure you can install a certificate for machines (web roles) by uploading the certificate under the hosted service which holds the web roles 
(machines) that runs the web application.
You also need to configure your cloud project with the certificate, refer the section 'Certificates Page' in 
http://msdn.microsoft.com/en-us/library/ee405486.aspx , note that you need to set Current User\Personal (My) as the Store Name.

















Note the thumbprint associated with the certificate and register this certificate as WIF ServiceCertificate via web.config. Add below line in web.config under: <configuration>/<system.identityModel.services>/<federationConfiguration> section:
<serviceCertificate>
   <certificateReference x509FindType="FindByThumbprint"
                              findValue="<cerrt-thumbnail>"
                              storeLocation="LocalMachine"
                              storeName="My"/>
 </serviceCertificate>


Solution - 2

In soultion-1 we registered an instance of 'RsaEncryptionCookieTransform' for performing encryption and decryption of cookies using 
shared certificate across all machines.

Another option is to use machine key for encrypt-decrypt. The idea is to use the machine key configured in web.config.

When you deploy your application to Azure, as a part of hosting the application, infrastructure will takes care of inserting a <machineKey> node 
in web.config.

<machineKey decryption="AES" decryptionKey="<decryptionKey>" validation="SHA1" validationKey="<validationKey>" />

Infrastructure will ensure 'decryptionKey' and 'validationKey' are same across all role instances (machines).
ASP.NET 4.5 provide APIs which uses these machineKey, for example MachineKey.Protect() protects the specified data by encrypting or signing
it using keys present in <machineKey>. Similarly MachineKey.Unprotect() the specified data that was protected by the Protect method.


Now we can write our own 'CookieTransform' using MachineKey APIs.

public class MachineKeyTransform : CookieTransform
{
    public override byte[] Encode(byte[] value)
    {
        if (value == null)
        {
           throw new ArgumentNullException("value");
        }

        return Encoding.UTF8.GetBytes(MachineKey.Protect(value, 
    "protectme"));
    }


    public override byte[] Decode(byte[] encoded)
    {
        if (encoded == null)
        {
            throw 
  new ArgumentNullException("encoded");
        }

        return MachineKey.Unprotect(Encoding.UTF8.GetString(encoded), 
    "protectme");
    }
}

We can now use instance of this class instead of RsaEncryptionCookieTransform in 'OnFederationConfigurationCreated'
Okay, that being said, in .NET 4.5 we don't have to do any of the above steps :), the latest WIF comes with Session Handler which uses machine key
as described above.
All we have to do is in web.config use built-in 'MachineKeySessionSecurityTokenHandler' instead of  'SessionSecurityTokenHandler'.
In this case we don't have to register for the event 'FederationConfigurationCreated' and register any 'Transform' instance.

<system.identityModel>
  <identityConfiguration>
    <securityTokenHandlers>
      <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler,       
        System.IdentityModel, Version=4.0.0.0, 
        Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, 
        System.IdentityModel.Services, Version=4.0.0.0, 
        Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </securityTokenHandlers>
  </identityConfiguration>
</system.identityModel>

No comments:

Post a Comment