It’s official: I am getting old. I was ab-so-lu-te-ly convinced I already blogged about this, but after the nth time I got asked about this I came to find the link to the post only to find pneumatic vacuum in its place. No joy on the draft folder either… oh well, this won’t take too long to (re?)write anyway. Moreover: in a rather anticlimactic move, I am going to give away the solution right at the beginning of the post.
Straight to the Point
In order to run in Windows Azure Web Sites a Web application which uses WIF for handling authentication, you must change the default cookie protection method (DPAPI, not available on Windows Azure Web Sites) to something that will work in a farmed environment and with the IIS’ user profile load turned off. Sounds like Klingon? Here there’s some practical advice:
- If you are using the Identity and Access Tools for VS2012, just go to the Configuration tab and check the box “Enable Web farm ready cookies”
- If you want to do things by hand, add the following code snippet in your system.identitymodel/identityConfiguration element:
2: <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler,
System.IdentityModel.Services, Version=220.127.116.11, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
3: <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler,
System.IdentityModel, Version=18.104.22.168, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
I know, it’s clipped and wrapped badly: eventually I’ll change the blog theme. For the time being, you can copy & paste to see the entire thing.
Note, the above is viable only if you are targeting .NET 4.5 as it takes advantage of a new feature introduced only in the latest version. If you are on .NET 4.0 WIF 1.0 offers you the necessary extensibility points to reproduce the same capability (more details below) however there’s nothing ready out of the box (you could however take a look at the code that Dominick already nicely wrote for you here).
Now that I laid down the main point, in the next section I’ll shoot the emergency flare that will lead your search engine query here.
Let’s go through a full cycle of create a WIF app -> deploy it in Windows Azure Web Sites –> watch it fail –> fix it –> verify that it works.
Fire up VS2012, create a web project (I named mine “AlphaScorpiiWebApp”, let’s see who gets the reference ;-)) and run the Identity and Access Tools on it. For the purpose of the tutorial you can pick the local development STS option. Hit F5, and verify that things work as expected (== the Web app will greet you as ‘Terry’).
Did it work? Excellent. Let’s try to publish to Windows Azure Web Site and see what happens. But before we hit Publish, we need to adjust a couple of things. Namely, we need to ensure that the application will communicate to the local development STS the address it will have in Windows Azure Web Sites, rather than the one on localhost:<port> automatically assigned (ah, please remind me to do a post about realm vs. network addresses). That’s pretty easy: go back to the Identity & Access tool, head to the Configure tab, and paste in the Realm and Audience fields the URL of your target Web Site.
Given what we know, we better turn off the custom errors before publishing (usual <customErrors mode="Off">under system.web).
Done that, go ahead and publish. My favorite route is through the Publish… entry in the Solution Explorer, I have a .PublishSettings file I import every time. Ah, don’t forget to check the option “Remove additional files at destination”.
Ready? Hit Publish and see what happens.
Boom! As expected, the authentication fails. Let me paste the error message here, for the search engines’ benefit.
Server Error in '/' Application.
The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread's user context, which may be the case when the thread is impersonating.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Security.Cryptography.CryptographicException: The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread's user context, which may be the case when the thread is impersonating.
[CryptographicException: The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread's user context, which may be the case when the thread is impersonating.] System.Security.Cryptography.ProtectedData.Protect(Byte userData, Byte optionalEntropy, DataProtectionScope scope) +379 System.IdentityModel.ProtectedDataCookieTransform.Encode(Byte value) +52 [InvalidOperationException: ID1074: A CryptographicException occurred when attempting to encrypt 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. ] System.IdentityModel.ProtectedDataCookieTransform.Encode(Byte value) +167 System.IdentityModel.Tokens.SessionSecurityTokenHandler.ApplyTransforms(Byte cookie, Boolean outbound) +57 System.IdentityModel.Tokens.SessionSecurityTokenHandler.WriteToken(XmlWriter writer, SecurityToken token) +658 System.IdentityModel.Tokens.SessionSecurityTokenHandler.WriteToken(SessionSecurityToken sessionToken) +86 System.IdentityModel.Services.SessionAuthenticationModule.WriteSessionTokenToCookie(SessionSecurityToken sessionToken) +148 System.IdentityModel.Services.SessionAuthenticationModule.AuthenticateSessionSecurityToken(SessionSecurityToken sessionToken, Boolean writeCookie) +81 System.IdentityModel.Services.WSFederationAuthenticationModule.SetPrincipalAndWriteSessionToken(SessionSecurityToken sessionToken, Boolean isSession) +217 System.IdentityModel.Services.WSFederationAuthenticationModule.SignInWithResponseMessage(HttpRequestBase request) +830 System.IdentityModel.Services.WSFederationAuthenticationModule.OnAuthenticateRequest(Object sender, EventArgs args) +364 System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +136 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +69
Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.17929
What happened, exactly? It’s a well-documented phenomenon. By default, WIF protects cookies using DPAPI and the user store. When your app is hosted in IIS, its AppPool must have a specific option enabled (“Load User Profile”) in order for DPAPI to access the user store. In Windows Azure Web Sites, that option is off (with good reasons, it can exact a heavy toll on memory) and you can’t turn it on. But hey, guess what: even if you could, that would be a bad idea anyway. The default cookie protection mechanism is not suitable for load balanced scenarios, given that every node in the farm will have a different key: that means that a cookie protected by one node would be unreadable from the other, breaking havoc with your Web app session.
What to do? In the training kit for WIF 1.0 we provided custom code for protecting cookies using the SSL certificate of the web application (which you need to have anyway) – however that is an approach more apt to the cloud services (where you have full control of your certs) rather than WA Web Sites (where you don’t). Other drawbacks included the sheer amount of custom (code required (not staggering, but non-zero either), its complexity (still crypto) and the impossibility of making it fully boilerplate (it had to refer to the coordinates of the certificate of choice).
In WIF 4.5 we wanted to support this scenario out of the box, without requiring any custom code. For that reason, we introduced a cookie transform class that takes advantage of the MachineKey, and all it needs to opt in is a pure boilerplate snippet to be added in the web.config. Sometimes I am asked why we didn't change that to be the default, to which I usually answer: I have people waiting for me at the cafeterias to yell at me for having moved classes under different namespaces (sorry, that’s the very definition of “moving WIF into the framework” :-)), now just imagine what would have happened if we would have changed the defaults :-D.
More seriously: you already know what the fix is: it’s one of the two methods described in the “Straight to the Point” section. Apply one of those, then re-publish. You’ll be greeted by the following:
We are Terry again, but this time, as you might notice in the address bar… in the cloud!
Alrighty, hopefully that was pretty straightforward. As usual, have fun!