Hopefully this entry serves as some decent documentation on how to write a Python client that accesses a web service which uses WS-Security. When I was trying to figure it out, Otu Ekanem’s response on the mailing list was invaluable. The example is relevant for any web service framework independent of programming language. This is tested with XFire 1.2.4 but can be used with .NET or other Java web service frameworks like Axis2.
When accessing a web service which has WS-Security enabled you must send very specific headers as part of your SOAP envelope in order for the request to be processed. You can read all about the glorious specification in PDF Format if you like. I’m using the Zolera Soap Infrastructure (ZSI) Library for Python which supports client stub generation. Given the generated stubs, there are two ways of adding custom headers to outgoing SOAP messages.
Method 1 – Not desirable but worth a mention
The first method involves modifying the generated code which is highly undesirable. Using the very simple SportsService web service example, you must modify the generated SportsService_client.py and edit the following line:
self.binding.Send(None, None, request, soapaction="", **kw)
self.binding.Send(None, None, request, soapaction="", soapheaders=(obj1,obj2) )
where obj1 and obj2 are instances of Python objects which are serialized as part of the SOAP header. I found this way to be tedious as you have to design your classes to match the SOAP header and write additional serialization code. It is also hard to create the exact header as namespaces and prefixes tend to be a problem.
Method 2 – Probably the way to go, way more customizable
We can use DOM-like methods to modify the SOAP header and send out exactly what we need. The example implements the UsernameToken strategy but other ones can also be implemented by modifying the headers in a similar manner. The generated Port class’ binding attribute has a sig_handler attribute which can be assigned an instance of a custom class. In this custom class, we must implement two methods, sign and verify, that can modify the header and check it’s validity, respectively. The sign method takes in as argument a SoapWriter which enables us to modify the header. So without further ado, here’s the class that adds WS-Security headers to the outgoing SOAP envelope as discussed above. The code has been formatted and modified to fit the page.
# Deprecated in 2.5, use the hashlib module instead: # http://docs.python.org/lib/module-hashlib.html import sha import binascii import base64 import time import random class SignatureHandler: OASIS_PREFIX = "http://docs.oasis-open.org/wss/2004/01/" + "oasis-200401" SEC_NS = OASIS_PREFIX + "-wss-wssecurity-secext-1.0.xsd" UTIL_NS = OASIS_PREFIX + "-wss-wssecurity-utility-1.0.xsd" PASSWORD_DIGEST_TYPE = OASIS_PREFIX + "-wss-username-token-profile-1.0#PasswordDigest" PASSWORD_PLAIN_TYPE = OASIS_PREFIX + "-wss-username-token-profile-1.0#PasswordText" def __init__(self, user, password, useDigest=False): self._user = user self._created = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time())) self._nonce = sha.new(str(random.random())). digest() if (useDigest): self._passwordType = self.PASSWORD_DIGEST_TYPE digest = sha.new(self._nonce + self._created + password).digest() # binascii.b2a_base64 adds a newline at the end self._password = binascii.b2a_base64(digest)[:-1] else: self._passwordType = self.PASSWORD_PLAIN_TYPE self._password = password def sign(self,soapWriter): # create element securityElem = soapWriter._header. createAppendElement("", "wsse:Security") securityElem.node. setAttribute("xmlns:wsse", self.SEC_NS) securityElem.node. setAttribute("SOAP-ENV:mustunderstand", "1") # create element usernameTokenElem = securityElem. createAppendElement("", "wsse:UsernameToken") usernameTokenElem.node. setAttribute("xmlns:wsse", self.SEC_NS) usernameTokenElem.node. setAttribute("xmlns:wsu", self.UTIL_NS) # create element usernameElem = usernameTokenElem. createAppendElement("", "wsse:Username") usernameElem.node. setAttribute("xmlns:wsse", self.SEC_NS) # create element passwordElem = usernameTokenElem. createAppendElement("", "wsse:Password") passwordElem.node. setAttribute("xmlns:wsse", self.SEC_NS) passwordElem.node. setAttribute("Type", self._passwordType) # create element nonceElem = usernameTokenElem. createAppendElement("", "wsse:Nonce") nonceElem.node. setAttribute("xmlns:wsse", self.SEC_NS) # create element createdElem = usernameTokenElem. createAppendElement("", "wsse:Created") createdElem.node. setAttribute("xmlns:wsse", self.UTIL_NS) # put values in elements usernameElem. createAppendTextNode(self._user) passwordElem. createAppendTextNode(self._password) # binascii.b2a_base64 adds a newline at the end nonceElem. createAppendTextNode( binascii.b2a_base64(self._nonce)[:-1]) createdElem.createAppendTextNode(self._created) def verify(self,soapWriter): self
Example usage of this is:
from SportsService_client import * from SportsService_types import * locator = SportsServiceLocator() port = locator.getSportsServiceHttpPort() sigHandler = SignatureHandler("user", "password", True) port.binding.sig_handler = sigHandler request = getMascotRequest() teamObj = ns0.Team_Def("Team") teamObj._name = "toronto" request._team = teamObj response = port.getMascot(request) print response._out._name
As you can see the SignatureHandler class is implementing an “interface” which enables it to process outgoing SOAP Requests. The verify method is empty but can contain code to check whether the SOAP header is valid.
If you would like write a PHP client that accesses a WS-Security enabled service, you should read Kim Cameron’s IdentityBlog entry which has links to the source code needed. If you simply want to use a PHP client for a non WS-Security web service, an earlier blog entry covers that.