Friday, April 19, 2013

ASP.NET Mixed-mode Authentication using Forms and Azure ACS in .NET 4.5

I recently found myself needing to upgrade an ASP.NET 4.5 application that was able to use Forms authentication and regular old Membership Providers for one class of users, and use Federated Identity with Azure ACS for another class of users. I assumed this would be fairly simple because most web sites do something like this, but most of the information I found pointed to the idea that I'd need to write my own STS and do away with Membership entirely.

It's odd that this was complicated, because when you create a new  ASP.NET application, the default login page says "Enter your username/password or log in with one of these providers". I know... this is ASP.NET's OAuth authentication which is somehow different from using Azure ACS and Federated Authentication... The point is, it's not that complicated, but it took me a long time to find all the needed info, so I will compile it here.

Prerequisites

I will start by saying that there was one website in particular that really got me past the point where I was stuck, and it was this one: http://netpl.blogspot.com/2011/08/quest-for-customizing-adfs-sign-ing-web.html A lot of what is discussed in this blog is the same kind of stuff I did. The only problem is that it is referring to .NET 3.5, and in .NET 4.5 Microsoft integrated WIF into the .NET Framework. This doesn't just mean the names of the assemblies changed from Microsoft.IdentityModel to System.IdentityModel. It also turns out that FederatedPassiveSignIn control that is the key to everything Wiktor sets up in his article no longer exists.

So here are the steps I took to get my system where I wanted it.
  1. Add references to System.IdentityModel and System.IdentityModel.Services
  2. Configure Azure ACS for your STS or other needs. This is a large topic in and of itself, but lots of documentation is available. Try starting here.
  3. Configure you STS using the Identity and Access Tool.
    1. Run the Identity and Access Tool by right-clicking your web project and selecting Identity and Access.
    2. Select "Use the Windows Azure Access Control Service" (or configure a different STS, I don't believe much of this is specific to Azure STS).
    3. Click the link that says "(Configure...)"
    4. Enter the ACS Namespace and the symmetric key for ACS Management:
      1. Go to https://yournamespace.accesscontrol.windows.net/
      2. Click Management Service
      3. Under "Credentials", select Symmetric Key
      4. There is a hidden key, with a button next to it that says "Show Key". Click it to get the key.
      5. Enter the Key in Visual Studio.
    5. Enter the Realm for your application.
  4. The Identity and Access Tool changed several things in web.config that we will need to undo.
    1. system.Web/authentication with mode="Forms" was commented out and replaced with one where mode="None". Switch back to the one with "Forms".
    2. In system.webServer/modules there was an element added: <remove name="FormsAuthentication" />. Comment or remove this line. We actually still need the FormsAuthentication module, because we're still using Forms Authentication.
    3. Everything else that the Identity and Access Tool changed can be left alone.
  5. You need to add a way for your users to log in to the STS. Here is the simplest way to do it. On your Login.aspx page, add a button:
    <asp:Button runat="server" ID="btnSTSLogin" Text="Click here to log in via the STS" OnClick="btnSTSLogin_Click" />
    
    And in the code behind, add this:
            protected void btnSTSLogin_Click(object sender, EventArgs e)
            {
                System.IdentityModel.Services.FederatedAuthentication.WSFederationAuthenticationModule.SignIn("CxPortal");
            }
    
    What you put in that argument does not matter. Literally. Read the Intellisense help.
  6. Now here's the real tricky part. When you log in with the FederatedAuthentication tool, it ONLY logs you in to the STS. Because we've gone back to using Forms authentication, you still need to set the Forms Authentication cookie. We do this by capturing WSFederationAuthenticationModule's OnSignedIn event. The MSDN documentation suggests the best place to put this is in Global.aspx under Application_Start. So here's the code I used:
            protected void Application_Start(object sender, EventArgs e)
            {
                // Set up Federated Authentication handlers.
                FederatedAuthentication.WSFederationAuthenticationModule.SignedIn += WSFederationAuthenticationModule_SignedIn;
            }
    
            void WSFederationAuthenticationModule_SignedIn(object sender, EventArgs e)
            {
                var principal = (ClaimsPrincipal)Thread.CurrentPrincipal;
                FormsAuthentication.SetAuthCookie(principal.Identity.Name, true);
            }
    
  7. If you handle the OnLoggedIn event in your Login control, you may need to do additional work to handle that. One consideration is that the HTTP Session is not available in the context of the Application_Start function, so you may have to get creative. I ended up checking on each request whether the user is initialized and initializing them if not. Your mileage may vary.
  8. Now you must handle logging out. There are two ways to log out: via your web application and via the STS. The basic idea is that you need to log out the user from the Forms authentication module AND from the Federated authentication module. How that is done depends on the how the log out is taking place.
    1. Via the Application: When a user clicks the Logout control in the application, it logs them out from the Forms Authentication module automatically. If they were a forms only user to begin with, then you are done. But if they were a federated user, you have to log them out of the STS as well so that they are logged out of any other applications they are signed into. To do this, handle the OnLoggedOut event of the Logout control (probably on the Master Page) with the following code:
              protected void LoginStatus_LoggedOut(object sender, EventArgs e)
              {
                  // If the user is signed on using Azure ACS, do a Federated Log Out.
                  if (Page.User.Identity.AuthenticationType == "Federation")
                  {
                      var fam = FederatedAuthentication.WSFederationAuthenticationModule;
      
                      // initiate a federated sign out request to the sts.            
                      SignOutRequestMessage signOutRequest = new SignOutRequestMessage(new Uri(fam.Issuer), fam.Realm + "Default.aspx");
      
                      // ACS requires the "wtrealm" parameter for Federated Sign Out, so add it.
                      signOutRequest.SetParameter("wtrealm", fam.Realm);
      
                      // Get the actual signout URL.
                      string signOutUrl = signOutRequest.WriteQueryString();
      
                      Response.Redirect(signOutUrl);
                  }
              }
      
      There is a function in WIF called WSFederationAuthenticationModule.FederatedSignOut() that in theory should be able to be used for this. However, when I call it I get an error that the wrealm parameter was not specified. The above code DOES work, so I use that instead.
    2. Via the STS: If the user logs out of the STS via some other application that is federated with you STS, they will need to be logged out of your application as well. The STS handles this by sending browser redirects to the various relying parties saying "Hey, you should sign out this user". When the redirected request is received by the application, it will automatically sign the user out from the Federated Module, but not the Forms module. So we need to handle the WSFederatedAuthenticationModule.OnSignedOut event, in the same way he handled to OnSignedIn event, so once again we will go to Global.asax and modify the Application_Start event to register the event handler:
              protected void Application_Start(object sender, EventArgs e)
              {
                  // Set up Federated Authentication handlers.
                  FederatedAuthentication.WSFederationAuthenticationModule.SignedIn += WSFederationAuthenticationModule_SignedIn;
                  FederatedAuthentication.WSFederationAuthenticationModule.SignedOut += WSFederationAuthenticationModule_SignedOut;
      
                  ...
              }
      
              void WSFederationAuthenticationModule_SignedOut(object sender, EventArgs e)
              {
                  FederatedAuthentication.SessionAuthenticationModule.SignOut();
                  FormsAuthentication.SignOut();
              }
      
And that's basically it. There is more that you may need to add, depending on the needs of your application. You may want to add code that automatically creates user accounts when someone logs in from the STS for the first time. You will probably have to reproduce any initialization that happens in the OnLoggedIn event, as mentioned in step 7. But the basic flow works.

Tuesday, April 9, 2013

How to encrypt and decrypt Azure Autoscaling Block Rules Store and Service Information Store in code

The Azure Autoscaling Block (WASABi) has a lot of configurability, but one common way is to store your Rules Store and Service Information Store as xml files and put them up in blob storage. You can also encrypt these XML files and provide the Autoscaling block with the thumbprint of the certificate that was used to encrypt. This is all described in more detail here.

The thing is, I wanted to write a web frontend that would allow authorized users to modify the rules or the service information store as needed. This means at the very least the ability to encrypt and decrypt those files from code. It took me awhile to figure this out, but you can directly access the encryption provider that the Autoscaling Block uses to do this encryption and call its encrypt and decrypt methods. If you write your own provider or use one other than the Pcks12ProtectedXmlProvider included in the Autoscaling Block this won't work, but here is the idea:

        private string EncryptXml(string thumbprint, string xml)
        {
            Microsoft.Practices.EnterpriseLibrary.WindowsAzure.Autoscaling.Security.Pkcs12ProtectedXmlProvider provider = 
                new Microsoft.Practices.EnterpriseLibrary.WindowsAzure.Autoscaling.Security.Pkcs12ProtectedXmlProvider(
                    System.Security.Cryptography.X509Certificates.StoreName.My, System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine,
                    thumbprint, false);
            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = true;
            doc.Load(new StringReader(xml));

            XmlNode encrypted = provider.Encrypt(doc.DocumentElement);
            return encrypted.OuterXml;
        }

        private string DecryptXml(string thumbprint, string xml)
        {
            Microsoft.Practices.EnterpriseLibrary.WindowsAzure.Autoscaling.Security.Pkcs12ProtectedXmlProvider provider = 
                new Microsoft.Practices.EnterpriseLibrary.WindowsAzure.Autoscaling.Security.Pkcs12ProtectedXmlProvider(
                    System.Security.Cryptography.X509Certificates.StoreName.My, System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine,
                    thumbprint, false);
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.PreserveWhitespace = true;
            xmlDoc.Load(new StringReader(xml));

            XmlNode decryptedNode = provider.Decrypt(xmlDoc.DocumentElement);
            return decryptedNode.OuterXml;
        }