How to call a Web Service from CF7 using Client Certificate based ("Bilateral") authentication (Win)
If you have to call a web service from ColdFusion 7 - and the destination web service enforces bilateral SSL authentication (i.e. the caller has to supply a client certificate to prove who they are, as well as the destination supplying a server certificate to prove who it is...) - then the options, in descending order of preference are:
- Check / ask really nicely / beg / etc. to see if there is a way of calling the web service without passing a client SSL certificate
- Use CF8 (or later) - I believe that you will still need to do the SOAP generation by hand, as cfobject / cfinvoke do not appear to allow a client certificate to be set - but you can use cfhttp and specify the clientcert and clientcertpassword attributes - which avoids having to use the WinHttp.WinHttpRequest COM object as shown below.
- Use the WinHttp.WinHttpRequest COM object as shown below...
ColdFusion (when running on windows) allows (and has allowed for several versions) COM objects to be called from within CF code, using the CFOBJECT tag or CreateObject() function.
Built into the following versions of windows, is the WinHTTP 5.1 COM object:
WinHTTP 5.1 is available only with Windows Server 2003, Windows XP with Service Pack 1 (SP1), and Windows 2000 Professional with Service Pack 3 (SP3). (I suspect that it may also be available in Windows 2008)
This provides similar functionality to CFHTTP - but at a lower level. In particular it allows a client certificate to be specified, using the SetClientCetificate function.
This is not as simple as it seems - where as in CF8 using CFHTTP you just have to specify the physical location of the client certificate file - the WinHttp object requires that the client certificate is first imported into a certificate store - and then the reference to the certificate provided to the SetClientCetificate function:
Specifies the location, certificate store, and subject of a client certificate
To cut a very long (well around 5 hours) story short, this is how I finally got it to work:
- Open the Certificates Snap-in in MMC:
- Start > Run MMC
- File > Add/Remove Snap-in
- Click Add...
- Select Certificates and click Add
- Select Computer account and click Finish
- Select Local computer and click Finish
- Click Close
- Click OK
- Expand Certificates (Local Computer) in the tree
- Select Personal
- Right Click on the folder, and select All Tasks > Import
- Browse to and Select the supplied PFX file
- Type in the password used to encrypt the key
- Do not select the Enable strong private key protection option
Having imported the certificate, you now need to reference it within your CF Code.
First, create a reference to the WinHTTP object:
Next open a connection to the web service. - specifying POST verb, URL and not asynchronous.
Now specify to use the client certificate to use:
This was the bit I found hardest to work out...
- The LOCAL_MACHINE bit is logical enough, and documented in several places
- The next bit to me is not obvious at all... the Personal certificate store is referenced as My
- The next bit is not completely obvious either - most documentation I could find specified to use the subject of the certificate - but more specifically this is the common name - CN - within the subject
Then you have to set a couple of HTTP headers:
First to specify that the content of the request (as its to a SOAP based web service) is XML:
Then to specify the web service action to be called. This is whatever is listed in the WSDL as the action for the method you are calling.
And finally, to submit the SOAP request content to the web service:
Note that we are passing in there a valid SOAP format XML packet, according to what is expected by the specific web service being called.
If you are struggling to parse the WSDL from the remote service in your head (what kind of geek are you!) then I can highly recommend that you download Storm - a free and open source tool for testing web services. This allows you to point at the destination web service, select the method, and specify the data you want to submit for each parameter. You can then view the Raw XML for the SOAP request - and use that as a template for the web service request you are submitting.
Having successfully submitted the request, you may want to process the response
myxmldoc = XmlParse(#objWinHttp.ResponseText#);
myXMLinnerDoc = XmlParse(myxmldoc["soap:Envelope"]["soap:Body"].xmlchildren.xmlchildren.xmltext);
The response is returned as a string within objWinHttp.ResponseText - which as it is a SOAP response, will be XML. The specific code for processing the response is obviously dependant on the web service - but the code above should give access the actual response within the SOAP wrapper.
Hopefully this will save someone some time - it took about 9 hours in total to work out what is and is not possible within CF7 - and having found a solution which should work, to actually work out the specific combination of steps necessary
Please note, all the code above is very much AS IS - error handling is always good - especially when there's a dependency on another service! etc. etc.>
I'd be really interested to hear from anyone else who has had a similar requirement - and how they approached it. I'd especially like to see any better ways of achieving the same!