Saturday, January 4, 2014

Securing a Web Service with Azure ACS

The Identity and Access Add-In to Visual Studio does a great job of securing a web site with a variety f mechanisms including Azure ACS.  I was bitterly disappointed to find it did not offer the same ability for a Web Service.  

In the end my salvation came by using the Sentinet Service Repository which does allow me to virtualize the web service and can include authentication with ACS.  It does so using binding configuration and this extract does the job of providing ACS authentication.  Note that you need to be using HTTPS protocol.

I've not had a chance yet to add this directly to a web service to see if it works.  My hope is that just adding it will be enough and then all I need to do is pass in the user name and password when I call the web service.  What ACS will do is produce a SAML token which will be encrypted within the SOAP message. 

 <bindings>
  <customBinding>
    <binding name="IssuedToken">
      <security authenticationMode="IssuedToken">
        <issuedTokenParameters>
          <issuerMetadata address="
https://mynamespace.accesscontrol.windows.net/v2/wstrust/mex" />
          <issuer address="
https://mynamespace.accesscontrol.windows.net/v2/wstrust/13/username" binding="ws2007HttpBinding" bindingConfiguration="AcsBinding" />
        </issuedTokenParameters>
      </security>
      <httpTransport />
    </binding>
  </customBinding>
  <ws2007HttpBinding>
    <binding name="AcsBinding">
      <security mode="TransportWithMessageCredential">
        <message clientCredentialType="UserName" negotiateServiceCredential="true" algorithmSuite="Default" establishSecurityContext="false" />
      </security>
    </binding>
  </ws2007HttpBinding>
</bindings>
   

Calling a WCF Web Service secured with Azure ACS


This is some sample code for a client console application calling a WCF web service that is secured with Windows Azure Access Control Service.  In fact all I need is to pass the username and password in the client credentials and my web service will do the authentication for me.  If I supply an incorrect password then a MessageSecurityException is created so I need to ensure I have captured that an handled it in some way.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ACE.TestClient.ACS
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                VirtualService.VirtualInterfaceClient c = new VirtualService.VirtualInterfaceClient();
                c.ClientCredentials.UserName.UserName = "MyService";
                c.ClientCredentials.UserName.Password = "Hydrogen1";

                VirtualService.Event input = new VirtualService.Event() { Date = DateTime.Now, Id = "123", Name = "Peter Rabbit" };
                string result = c.PostEvent(input);
                Console.WriteLine("Service Returned: " + result);
            }
            catch (System.ServiceModel.Security.MessageSecurityException mex)
            {
                Console.WriteLine("Failed to authenticate." );
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

Calling a REST Web Service with an X509 certificate

I was recently looking at what client code I needed in order to call a REST web service that was secured with an X509 certificate.

In the first case I assume that the client device (maybe a tablet) has an X509 certificate installed.  So what code do I need to send some JSON to this REST web service and include the X509 certificate.  The simple example below works.  After that you will see the sample code for sending a user name and password with

using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;

namespace TestClientBizTalkService
{
    class Program
    {
        const string JsonPayload = "{\"ns0:Event\":{\"@xmlns:ns0\":\"
http://RESTDEMO.Event\",\"Id\":\"444\",\"Date\":\"1999-05-31\",\"Name\":\"A new event\"}}";
        static void Main(string[] args)
        {
            try
            {
                WebClientWithSslCertificate c = new WebClientWithSslCertificate();
               
                c.Headers[HttpRequestHeader.ContentType] = "application/json";
                string result = c.UploadString("
https://acesentinetpot/SelfHostedNode/BizTalkRestMutualX509", "POST", JsonPayload);
                Console.WriteLine("Service Returned: " + result);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            Console.WriteLine("Done");
            Console.ReadLine();
        }

        class WebClientWithSslCertificate : WebClient
        {
            protected override WebRequest GetWebRequest(Uri address)
            {
                HttpWebRequest request = (HttpWebRequest)base.GetWebRequest(address);
                request.ClientCertificates.Add(GetMyCertificate());
                return request;
            }

            private X509Certificate2 GetMyCertificate()
            {
                X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

                try
                {
                    store.Open(OpenFlags.OpenExistingOnly);
                    X509Certificate2Collection collection = (X509Certificate2Collection)store.Certificates;
                    X509Certificate2Collection fcollection = (X509Certificate2Collection)collection.Find(X509FindType.FindBySubjectName, "ClientTestCertificate", true);
                    if (fcollection.Count > 0)
                    {
                        return fcollection[0];
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
                finally
                {
                    if (store != null)
                    {
                        store.Close();
                    }
                }
               
                return null;
            }
        }
       
    }
}

Tuesday, December 31, 2013

Configure BizTalk Receive Location with net.tcp

I just spent ages trying to change a receive location from basic Http to use net.tcp. When I tried to access the web service in IE I kept getting the message "The Messaging Engine failed to register the adapter for 'WCF-CustomIsolated' for the receive location...".

As usual it asks whether the receive location was started (it was) and if the isolated adapter runs under an account that has access to the BizTalk databases (which it does).
I had already followed the step to add the non-HTTP protocols and configured the web site to support net.tcp. I also configured the virtual directory to support http and net.tcp protocols.

To recap when creating the Receive Location I used the WCF-CustomIsolated adapter. I set the Binding to netTcpBinding and set the security mode to None to match the Anonymous access setting in IIS.
The problem was with the address Uri. I was using the address of net.tcp://localhost:808/path/service.svc which is shown in the examples. Changing the address Uri to /path/service.svc solved the problem.

Now when openeing the service in IIS I was able to see the usual service screen (using the http address) and view the wsdl. BTW, I wasn't able to get this to work with the WCF-NetTcp adapter because that too insists on the net.tcp:// address format.

But the next problem came when trying to create a client application to send messages to the net.tcp endpoint. I created a console application in Visual Studio but could not add the Service reference because it would not recognise the wsdl. So I switched the endpoint back to basicHttp and created the service reference in my console application and checked that it worked with basicHttp.

So then I switched the Receive Location back to net.tcp and changed the bindings and client endpoint in my console app to use net.tcp. I thought that was it but when I sent the message it would just timeout.

That's when I looked at the settings on the Receive Location again and realised maxConnections was 0 and the ReadQuota values were all zero.

These are what I set in the app.config of my client
<binding listenbacklog="16" maxconnections="16">

< maxDepth="32" maxStringContentLength="2147483646" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="2147483646"/>

I also added the security section to match that of the server.
<security mode="None">
Finally that did the trick and I was able to submit messages with net.tcp.

Wednesday, April 10, 2013

Submit XML message to BizTalk WCF Receive Web Service

I have been exposing orchestrations and schemas as WCF Web Services and I've been struggling recently in submitting messages to those web services. 
The usual approach I use is to create a test harness (a console app or win forms) and create a service reference to it. Then I go through the steps of creating the request message. My particular issue was trying to create an envelope with an embedded message.
I already had a sample XML message that worked and I found this excellent post which explains how to post an XML message to a WCF Web Service in BizTalk.  No service reference is required and there are very few lines of code. I'm going to use this method a lot more particularly where I have big messages because setting the value of each element can be time consuming.

Follow the steps in the post because they worked for me.  I'm reposting because I wanted to include the code for submitting to a WCF with basicHttp binding. Note the all important MessageVersion which must be set to Soap11.

public class SendBizTalk

{
[ServiceContract()]
private interface IBizTalkSubmission
{
         [OperationContract(Action = "*", ReplyAction = "*")]
         void Submit(Message msg);
}

static void Main(string[] args)
{
     XmlTextReader xmlrdr = new XmlTextReader(@"C:\Projects\Test\FedESB.Test1\Envelope2.xml");
    Message msg = Message.CreateMessage(MessageVersion.Soap11, "*", xmlrdr);

    string uriLocationEsbOnRamp = "http://localhost/FedESBWcfService2/Receive.svc";

     BasicHttpBinding b = new BasicHttpBinding();
     b.Security.Mode = BasicHttpSecurityMode.None;
     EndpointAddress epa = new EndpointAddress(uriLocationEsbOnRamp);
     IBizTalkSubmission proxy = ChannelFactory<IBizTalkSubmission>.CreateChannel(b, epa);
     proxy.Submit(msg);
}
}

Friday, March 22, 2013

BizTalk Deployment Framework

I've been using the BizTalk Deployment Framework in particular for highly available environments where there are 2 Receive hosts, 2 Send hosts and 2 Process hosts.  The 2 receive hosts are clustered which works well when you have orchestrations exposed as WCF web services because you can install the virtual directories on both servers and not have a single point of failure. 

The Deployment Framework allows you to deploy your BizTalk assemblies to the GAC and optionally to install them to the BizTalk management database.  So in this 6 server arrangement you select to deploy to the BizTalk management database on one of the servers and deploy just the assemblies to the GAC on the other 5.

Now the Deployment Framework provides the ability to install your virtual directories as part of the deployment but here is where you hit your first problem. It only does so when you select "deploy to management database" as true which means you only get them deployed onto one server.  And your deployment instructions need to be clear about which server you install the virtual directories on. 

I've deployed a lot of web services so I have experience in creating virtual directories and application pools.  The APPCMD is my best friend and can be called using

%systemroot%\system32\inetsrv\APPCMD

So I started to create a Windows CMD file called CreateVDir.cmd.  And it occurred to me I can use the %COMPUTERNAME% to specify which server I want to execute commands on.

IF NOT "%COMPUTERNAME"=="%RECEIVEHOST1%" GOTO END
IF NOT "%COMPUTERNAME"=="%RECEIVEHOST2%" GOTO END

In this case I am passing RECEIVEHOST1 and RECEIVEHOST2 as parameters to the CreateVDir.cmd along with the Virtual Directory name, the physical path and the user name and password for the Application Pool account.  And the great thing is I can have these as environment settings in the SSO and pass them into CreateVDir.cmd as $(VDIR_UserName) $(ReceiveHost1) etc.

So I just need to create a Target in BTDFPROJ as
<Target AfterTargets="" Name="CustomDeployTarget">
<Exec command="..\CreateVdir.cmd <vdirname> <physpath>  $(VDIR_UserName)  $(VDIR_UserPass) $(ReceiveHost1)  $(ReceiveHost2)"/>
</Target><

I also need to create a CustomUndeployTarget which calls my DeleteVdir.cmd file and then I have all the control I need. I like this approach. I keep my BTDFPROJ file simple and I have CMD files that I can create and test independently of the deployment process.

Good old DOS commands!

Monday, March 18, 2013

Create IIS Virtual Directory using APPCMD

The command line script below allows you to create a Virtual Directory from an existing physical directory and set up an application pool.  In this case the application pool will be running under a domain service account. 

To use an identity of Network Service then replace
/processModel.identityType:SpecificUser  ...
with
/processModel.identityType:NetworkUser


@echo OFF

IF "%1"=="" GOTO Syntax
IF "%2"=="" GOTO Syntax
IF "%3"=="" GOTO Syntax
IF "%4"=="" GOTO Syntax

set VDIRNAME=%1
set PHYPATH=%2
set USERNAME=%3
set PASSWORD=%4

REM Create Application Pool

%systemroot%\system32\inetsrv\APPCMD add apppool /name:%VDIRNAME%AppPool

%systemroot%\system32\inetsrv\APPCMD set apppool "%VDIRNAME%AppPool" /managedRuntimeVersion:v4.0

%systemroot%\system32\inetsrv\APPCMD set apppool "%VDIRNAME%AppPool" /managedpipelineMode:Classic

%systemroot%\system32\inetsrv\APPCMD set apppool "%VDIRNAME%AppPool" /processModel.identityType:SpecificUser /processModel.userName:%USERNAME% /processModel.password:%PASSWORD%

REM Add Virtual Directory

%systemroot%\system32\inetsrv\APPCMD add app /site.name:"Default Web Site" /path:/%VDIRNAME% /physicalpath:"%PHYPATH%"

%systemroot%\system32\inetsrv\APPCMD set app "Default Web Site/%VDIRNAME%" /applicationpool:%VDIRNAME%AppPool

:SYNTAX
ECHO.
ECHO VDir Name and Physical Path Required
ECHO.
ECHO CreateVDir.CMD VDirName C:\PhysPath Domain\UserName Password
ECHO example CreateVDir TEST c:\inetpub\wwwroot\test buildx\btsuser Password1