Edit and Apply New WIF’s Config Settings in Your Windows Azure WebRole… Without Redeploying!

In short: in this post I will show you how you can leverage the OnStart event of a WebRole to enable changing the WIF config settings even after deployment.

Since the very first time Hervey and I made the first foray in Windows Azure with WIF, all the way to the latest hands-on labs, books and whitepapers, one of the main challenges of using WIF in a WebRole has always been the impossibility of updating the settings in <microsoft.identityModel> without redeploying (or preparing in advance for a pool of alternative <service> elements fully known at deployment time).

Last Friday I was chatting with Wade about how to solve this very problem for some future deliverables in the toolkit, and it just came to me: why don’t we just leverage the WebRole lifecycle and use OnStart for setting the values we want even before WIF reads the web.config? All we need to do is create suitable <setting> entries in the ServiceConfiguration.cfg file, which can be modified without the need to redeploy, and use the events in WebRole.cs to ensure that our apps picks up the new values. Simple!

I created a new WebRole, hooked it to a local SelfSTS, and started playing with ServiceDefinition.csdef, ServiceConfiguration.cscfg and WebRole.cs. I just wanted to make sure the idea works, hence I didn’t pour much care in writing clean (or exhausting) code. Also, I totally ignored all the considerations about HTTPS, NLB session management and all those other things you learned you need to do in WIndows Azure. None of those really interferes with the approach, hence for the sake of simplicity I left them all out.

First, I created <Setting> entries  in the .csdef for every WIF config parameter generated by the Add STS Reference you’d likely want to control:

 <?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="WindowsAzureProject5" xmlns="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="WebRole1">
    <Runtime executionContext="elevated" />
    
    <!--... stuff-->
    <ConfigurationSettings>
      <Setting name="audienceUri" />
      <Setting name="issuer" />
      <Setting name="realm" />
      <Setting name="trustedIssuersThumbprint" />
      <Setting name="trustedIssuerName" />
    </ConfigurationSettings>
  </WebRole>
</ServiceDefinition>

Yes, yes, having settings just for one issuer in the trusted issuers registry is not especially elegant; and adding a homeRealm would probably be useful. Some other time.

The important thing to notice here is the <Runtime executionContext=elevated” /> . Without that, you won’t be able to save the modifications to the Web.Config.

Then I added the same settings in the .cscfg, leaving all the values empty (for now).

 <?xml version="1.0" encoding="utf-8"?>
<ServiceConfiguration serviceName="WindowsAzureProject5" xmlns="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="1" osVersion="*">
  <Role name="WebRole1">
    <Instances count="1" />
    <ConfigurationSettings>
      <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="UseDevelopmentStorage=true" />
      <Setting name="audienceUri" value="" />
      <Setting name="issuer" value="" />
      <Setting name="realm" value="" />
      <Setting name="trustedIssuersThumbprint" value="" />
      <Setting name="trustedIssuerName" value="" />
<!--...stuff-->
    </ConfigurationSettings>
    <!--...stuff-->
  </Role>
</ServiceConfiguration>

Very straightforward. Then I went ahead and added to WebRole.cs  the code below:

 namespace WebRole1
{
    public class WebRole : RoleEntryPoint
    {
        public override bool OnStart()
        {
            RoleEnvironment.Changing += RoleEnvironmentChanging;

                using (var server = new ServerManager())
                {
                    var siteNameFromServiceModel = "Web";
                    var siteName =
                        string.Format("{0}_{1}", RoleEnvironment.CurrentRoleInstance.Id, siteNameFromServiceModel);

                    string configFilePath = server.Sites[siteName].Applications[0].VirtualDirectories[0].PhysicalPath + "\\Web.config";
                    XElement element = XElement.Load(configFilePath);

                    string strSetting;

                    if (!(String.IsNullOrEmpty(strSetting = RoleEnvironment.GetConfigurationSettingValue("audienceUri"))))
                        element.Element("microsoft.identityModel").Element("service").Element("audienceUris").Element("add").Attribute("value").Value = strSetting;
                    if (!(String.IsNullOrEmpty(strSetting = RoleEnvironment.GetConfigurationSettingValue("issuer"))))
                        element.Element("microsoft.identityModel").Element("service").Element("federatedAuthentication").Element("wsFederation").Attribute("issuer").Value = strSetting;
                    if (!(String.IsNullOrEmpty(strSetting = RoleEnvironment.GetConfigurationSettingValue("realm"))))
                        element.Element("microsoft.identityModel").Element("service").Element("federatedAuthentication").Element("wsFederation").Attribute("realm").Value = strSetting;
                   
                    if (!(String.IsNullOrEmpty(strSetting = RoleEnvironment.GetConfigurationSettingValue("trustedIssuersThumbprint"))))
                        element.Element("microsoft.identityModel").Element("service").Element("issuerNameRegistry").Element("trustedIssuers").Element("add").Attribute("thumbprint").Value = strSetting;
                    if (!(String.IsNullOrEmpty(strSetting = RoleEnvironment.GetConfigurationSettingValue("trustedIssuerName"))))
                        element.Element("microsoft.identityModel").Element("service").Element("issuerNameRegistry").Element("trustedIssuers").Element("add").Attribute("name").Value = strSetting;

                    element.Save(configFilePath);
                }
           
                            
            return base.OnStart();
        }
        private void RoleEnvironmentChanging(object sender, RoleEnvironmentChangingEventArgs e)
        {
            e.Cancel = true;
        }
    }
}

Let’s look at what happens in the using block first. If you want to read good writeups on this technique I suggest this msdn entry or this really nice entry from Andy Cross.

When OnStart runs, the WebRole application itself didn’t have a chance to do anything yet. What I want to do here is getting my hands on the web.config file, override the WIF settings with all the non-empty values I find in ServiceConfiguration.cscfg and save back the file even before WIF gets to read <microsoft.identityModel>.

What I do above with Linq to XML for modifying the WIF settings is pretty dirty, very brittle and definitely tied to the assumption that the config we’ll be working with is the one that comes out from a typical Add STS Reference run. I tried to use ConfigurationManager at first, but it complained that <microsoft.identityModel> has no schema hence I just went the quicker, easier, more seductive “let’s just see if it works”. But remember, for the one among you who caught the reference: the dark side is not stronger. No no no.

Aaanyway. The element.Save(configFilePath) is the line that will fail if you forgot to add the elevated directive in the csdef, you’re warned.

The RoleEnvironmentChanging handler hookup at the beginning of OnStart, and the handler itself, are meant to ensure that when you change the values in ServiceConfiguration.cscfg Windows Azure will properly restart the role. If you don’t add that, just changing the config will not drive changes in the WebRole behavior until a stop & restart occurs. Technically there are few things you may try to do to get WIF to pick up the new settings at mid flight, but all those would entail changing the application code and that’s exactly what I am trying to avoid with all this brouhaha.

BTW, you can thank Nick Harris for the RoleEnvironment.Changing trickSmile

Nick just joined the Windows Azure Evangelism team and he is already doing an awesome job.

That should be all. Now, try to ignore the impulse that would make you change the config before deploying, and publish the project in Windows Azure staging “as is”.

image

In few mins the instance is up and running, listening at a nice (and totally unpredictable) URL https://eddb883659d04d0bbbb570f17c52ea01.cloudapp.net. What do you think will happen if I just navigate there?

image

That’s right. WIF is still configured for the address the application had in the environment formerly known as devfabric (now Windows Azure simulation environment), as described in the realm entry, hence SelfSTS (which behaves like the WIF STS template if there’s no wreply in the signin message) sends the token back there instead of https://eddb883659d04d0bbbb570f17c52ea01.cloudapp.net. Normally we’d be pretty stuck at this point, but thanks to the modification we made we can fix the situation.

All you need to do is navigating to the Windows Azure portal, select the deployment and hit the Configure button.

image

Here you can pick the Edit current configuration option to update the values inline. In this case, all you need to do is pasting https://eddb883659d04d0bbbb570f17c52ea01.cloudapp.net in the audienceUri and realm settings, and hit OK.

image

You’ll see the portal updating the instance for few moments. As soon as it reports the role as ready, navigate to its URL and, surprise surprise, this time the authentication flow ends up in the right place! In the screenshot below you can see (thanks to the SecurityTokenVisualizerControl, which you can find in all the latest ACS labs in the identity training kit) that the audienceURI has been changed as well.

image

I think that’s pretty cool Smile

Now, you may argue that this scenario is an artifact of how the WIF STS template handles things, and if you would have ben dealing with an STS (like ACS) which keeps realm and return URLs well separated you could have solved the matter at the STS side. All true, but beside the point.

Here I used the staging & realm example because with its unknowable-until-it’s-too-late GUID in the URL it is (was?) the paradigmatic example of what can be challenging when using WIF with Windows Azure; but of course you can use the technique you saw here for pushing out any post-deployment changes, including pointing the WebRole to a different STS, updating certificate thumbprints as keys rollover takes place or any other setting you may want to modify.

Please use this technique with caution. I haven’t used extensively yet hence I am not 100% sure if there are gotchas just waiting to be found, but so far it seems to be solving the problem pretty nicely Smile