Wednesday 18 April 2012

Federated custom tcp binding (and http binding)

This is a work in progress, but so far it allows me to take a token from ACS (Azure Access Control Services) - I followed the basic SDK/MSDN advice to set up a relying party, provider and rules - and send it to a service (acting as Relying Party) by way of authentication.

I was particularly interested to try this with TCP binding and it looks like custom binding is the only way.

For the http I started with the Federated HTTP binding, but since the custom binding took shape, you get more control this way, so I formulated an https equivalent here too.

Note this is for illustration purposes it's certainly not production ready.

1. Get token from ACS
  

        private static SecurityToken GetIdentityProviderToken(string acsEndpoint, string serviceEndpoint)
        {
            var factory =
                new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential), acsEndpoint)
                {
                    TrustVersion = TrustVersion.WSTrust13
                };

            factory.Credentials.ClientCertificate.SetCertificate(
                StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindBySubjectName,
                "[cert dns/hostname]");

            var rst = new RequestSecurityToken
            {
                RequestType = RequestTypes.Issue,
                AppliesTo = new EndpointAddress(serviceEndpoint),
                //specify URI realm that ACS token will apply to
                //AppliesTo = new EndpointAddress( new Uri( "urn:federation:customer:222:agent:11" ) ),
                KeyType = KeyTypes.Symmetric
            };

            factory.Credentials.UserName.UserName = ClientUsername;
            factory.Credentials.UserName.Password = ClientPassword;
            var channel = factory.CreateChannel();

            return channel.Issue(rst);
        } 


2. Http Client config (code)
 

        private static ChannelFactory GetCustomHttpBoundService(SecurityToken token, string address)
        {


            var securityBootStrap = SecurityBindingElement.CreateIssuedTokenForCertificateBindingElement(new IssuedSecurityTokenParameters());
            var security = SecurityBindingElement.CreateSecureConversationBindingElement(securityBootStrap, requireCancellation: true);
            
            Console.WriteLine("Include timestamp " + security.IncludeTimestamp);
            Console.WriteLine("Allow insecure transport " + security.AllowInsecureTransport);
            Console.WriteLine("Client: Detect replays " + security.LocalClientSettings.DetectReplays);
            Console.WriteLine("Client: Max clock skew " + security.LocalClientSettings.MaxClockSkew);
            Console.WriteLine("Server: Detect replays " + security.LocalServiceSettings.DetectReplays);
            Console.WriteLine("Server: Max clock skew " + security.LocalServiceSettings.MaxClockSkew);


            var customBinding = new CustomBinding(new List
            {  
                security,
                new BinaryMessageEncodingBindingElement(),
                new HttpsTransportBindingElement()
            });

            var factory = new ChannelFactory(customBinding,
                new EndpointAddress(new Uri(address), EndpointIdentity.CreateDnsIdentity("[cert dns/hostname]")));
            factory.ConfigureChannelFactory();

            Debug.Assert(factory.Credentials != null);
            factory.Credentials.SupportInteractive = false;
            factory.Credentials.ServiceCertificate.SetDefaultCertificate(StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindBySubjectName,
                "[cert dns/hostname]");

            return factory;
        }

3. TCP Client config (code)
  

        private static ChannelFactory GetCustomTcpBoundService(SecurityToken token, string address)
        {

            var securityBootStrap = SecurityBindingElement.CreateIssuedTokenForCertificateBindingElement(new IssuedSecurityTokenParameters());
            var security = SecurityBindingElement.CreateSecureConversationBindingElement(securityBootStrap, requireCancellation: true);
      
            Console.WriteLine("Include timestamp " + security.IncludeTimestamp);
            Console.WriteLine("Allow insecure transport " + security.AllowInsecureTransport);
            Console.WriteLine("Client: Detect replays " + security.LocalClientSettings.DetectReplays);
            Console.WriteLine("Client: Max clock skew " + security.LocalClientSettings.MaxClockSkew);
            Console.WriteLine("Server: Detect replays " + security.LocalServiceSettings.DetectReplays);
            Console.WriteLine("Server: Max clock skew " + security.LocalServiceSettings.MaxClockSkew);

            var customBinding = new CustomBinding(new List
            {  
                security,
                new BinaryMessageEncodingBindingElement(),
                new SslStreamSecurityBindingElement {RequireClientCertificate = false},
                new TcpTransportBindingElement()
            });

            var factory = new ChannelFactory(customBinding,
                new EndpointAddress( new Uri(address),  EndpointIdentity.CreateDnsIdentity("[cert dns/hostname]")));
            factory.ConfigureChannelFactory();
       
            Debug.Assert(factory.Credentials != null);
            factory.Credentials.SupportInteractive = false;
            factory.Credentials.ServiceCertificate.SetDefaultCertificate(StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindBySubjectName,
                "[cert dns/hostname]");

            return factory;
        }


4. Http Server config
  


        <binding name="customfedhttps">
          <security authenticationmode="SecureConversation" requiresecuritycontextcancellation="true">
            <secureconversationbootstrap authenticationmode="IssuedTokenForCertificate">
            </secureconversationbootstrap>
          </security>
          <binarymessageencoding>
          <httpstransport requireclientcertificate="false">
        </httpstransport></binarymessageencoding>
       </binding>

5. TCP Server config
  


        <binding name="customfedtcp">
          <security authenticationmode="SecureConversation" requiresecuritycontextcancellation="true">
            <secureconversationbootstrap authenticationmode="IssuedTokenForCertificate">
            </secureconversationbootstrap>
          </security>
          <binarymessageencoding>
          <sslstreamsecurity requireclientcertificate="false">
          <tcptransport>
        </tcptransport></sslstreamsecurity></binarymessageencoding>
        </binding>

6. Server behaviour
  

    <behaviors>
      <servicebehaviors>
        <behavior name="fedbehaviour">

          <servicemetadata httpsgetenabled="true">
          <federatedservicehostconfiguration>
          
        </federatedservicehostconfiguration></servicemetadata></behavior>
      </servicebehaviors>
    </behaviors>

    <extensions>
      <behaviorextensions>
        <add name="federatedServiceHostConfiguration" type="Microsoft.IdentityModel.Configuration.ConfigureServiceHostBehaviorExtensionElement, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      </add></behaviorextensions>
    </extensions>


7. Identity model config
  

  <microsoft.identitymodel>
    <service>
      <audienceuris>
        <add value="https://localhost/Service1.svc">
        <add value="net.tcp://localhost:997/Service2.svc">
      </add></add></audienceuris>

      <servicecertificate>
        <certificatereference findvalue="[cert dns/hostname]" storelocation="LocalMachine" storename="My" x509findtype="FindBySubjectName">
      </certificatereference></servicecertificate>

      <issuernameregistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedissuers>
          <add name="[cert dns/hostname]" thumbprint="[cert thumb print]">
        </add></trustedissuers>
      </issuernameregistry>

      <certificatevalidation certificatevalidationmode="None">

      
    </certificatevalidation></service>
  </microsoft.identitymodel>



Useful links:








Sunday 15 April 2012

Azure Web roles, configuring IIS - basic auth / ssl example

Windows Azure roles have the minimum configuration which covers most needs, but when you need to ensure that your deployment works with additional features some configuration is required. The best place to do this is when the web role starts. In this example I want to ensure that basic authentication is possible with my azure hosted website against a local NT account; so the basic authentication role module needs to be added to IIS7 when the role starts, I also need to unlock the config sections so that my web.config can configure the authentication (disable anonymous, enable basic authentication and allow SSL). Step 1. Create the file in the startup folder, first off allow powershell: enablepowershell.cmd - containing a single line:
  
powershell -command "set-executionpolicy Unrestricted"&

Step 2. A command file to add basic-auth and "unlock" the IIS config sections (easy one to forget!) that I require here:

configiis.cmd
  
ServerManagerCmd.exe -install web-basic-auth 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/access 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/authentication/anonymousAuthentication 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/authentication/basicAuthentication 


The basic principle can be extended to cover other scenarios. Step 3. Finally, some powershell to add a user "AddUser.ps1", in this example I'll configure the basic authentication such that a local user account provides the credentials, but as such an account doesn't yet exist, I will need to create it :

  $username="test"
  $computer = [ADSI]"WinNT://localhost"
  $user_obj = $computer.Create("user", "$username")
  $user_obj.SetPassword("testpassword1!")
  $user_obj.SetInfo()
  Write-Host "$username created."


Plenty of examples out there about how to make this a bit tighter. Now for tying this in with my azure web role. First off I want to up the OS version in my azure config (.cscfg) to version 2 (2008R2 I believe)

<ServiceConfiguration serviceName="AzureMocks" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="2" osVersion="*" >
.
.
</ServiceConfiguration >


Next I need to add some sections to my service definition file (.csdef) This runs the command files under elevated privs.

    <Startup>
      <Task commandLine="Startup\enablepowershell.cmd" executionContext="elevated" />
      <Task commandLine="Startup\configiis.cmd" executionContext="elevated" />
    </Startup >
    <Runtime executionContext="elevated" />

Lastly in my web role start I call the powershell "AddUser.ps1"

            
            try
            {
                Startup.RunPowershellConfig(@".\startup\AddUser.ps1", "AddUser.ps1.txt");

            }
            catch (Exception e)
            {
                RoleDiagnosticsHelper.WriteExceptionToBlobStorage(e, @"An error occured running the powershell startup script '.\startup\AddUser.ps1'");
                return false;
            }
Where those functions are defined as
  

        public static void RunPowershellConfig(string path, string outPath)
        {
            var startInfo = new ProcessStartInfo
            {
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                FileName = "powershell.exe",
                Arguments = path,
                RedirectStandardOutput = true,
                UseShellExecute = false,
            };

            var writer = new System.IO.StreamWriter(outPath);
            var process = Process.Start(startInfo);

            process.WaitForExit();

            writer.Write(process.StandardOutput.ReadToEnd());
            writer.Close();
        }

        public static void WriteExceptionToBlobStorage(Exception ex, string additionalInfo)
        {
            if (null == additionalInfo)
                additionalInfo = string.Empty;

            var storageAccount = CloudStorageAccount.Parse(
                RoleEnviroment.GetConfigurationSettingValue(
                  "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"));

            var container = storageAccount.CreateCloudBlobClient()
              .GetContainerReference("rolestartexceptions");
            container.CreateIfNotExist();

            var blob = container.GetBlobReference(string.Format(
              "role-start-exception-{0}-{1}.log",
               RoleEnvironment.CurrentRoleInstance.Id,
               DateTime.UtcNow.ToLongDateString()));
            
            // tostring should include inner exception if exists
            blob.UploadText(ex + " Additional information " + additionalInfo);
        }


I am careful to ensure that I catch and write any exceptions, a problem on role start can be tricky to find and fix otherwise. See cweyers post here
Of course I should not forget, my website config needs the required settings to actually allow SSL and basic auth now all the above are complete


  <system.webServer>
    <security xdt:Transform="Replace">
      <access sslFlags="Ssl" />
      <authentication>
        <anonymousAuthentication enabled="false"/>
        <basicAuthentication enabled="true"/>
      </authentication>
    </security>
  </system.webServer>


Important note
When running all this locally in the emulator that last bit of config can cause some problems, if you spin the site or service up in the emulator the debugger may refuse to attach (we've reported this as a bug under tools version 1.6 Nov 2011 and I believe it's known. The work around is to comment the offending section and when the emulator role starts, go into IIS and configure through the management console. Can use a config transform to ensure the config really does go in for real as this issue appears to be limited to the emulator. So if you see a weird emulator error complaining about invalid site, the config is a good place to start.