Axis2C, WS-Security & Perl

I've been wanting to build a Perl client to consume an Axis2C web service protected by Rampart/C for a while now.  So over the past couple days I took a few hours to figure out how.  First I needed a username/password protected service to test with.  I decided to use the "math" sample service that comes with the axis2c download.  I got axis2c, along with a bunch of other related stuff from WSO2's <a href="http://wso2.com/products/web-services-framework/c/" >Web Services Framework for C</a>.  For the most part, this just packages up a lot of Apache open source software like Axis2C and Rampart/C into one tidy tar file.  Anyway, the math sample service doesn't come configured with rampart security right out of the gate, so the first thing I had to do was modify the services.xml file in $WSFC_HOME/services/math to add it.  Here's what my file looked like after adding the entries for Rampart:

<service name="math">
 <parameter name="ServiceClass" locked="xsd:false">math</parameter>
 <description>
   This is a testing service, named 'math' to test multiple operations in the same service
 </description>
 <module ref="rampart"/>

 <wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
   <wsp:ExactlyOne>
     <wsp:All>
       <sp:AsymmetricBinding xmlns:sp="
http://schemas.xmlsoap.org/ws/2005/07/securitypolicy">
         <wsp:Policy>
           <sp:InitiatorToken>
             <wsp:Policy>
               <sp:X509Token sp:IncludeToken="
http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/Never">
                 <wsp:Policy>
                   <sp:WssX509V3Token10/>
                 </wsp:Policy>
               </sp:X509Token>
             </wsp:Policy>
           </sp:InitiatorToken>
           <sp:RecipientToken>
             <wsp:Policy>
               <sp:X509Token sp:IncludeToken="
http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/Never">
                 <wsp:Policy>
                   <sp:WssX509V3Token10/>
                 </wsp:Policy>
               </sp:X509Token>
             </wsp:Policy>
           </sp:RecipientToken>
           <sp:Layout>
             <wsp:Policy>
               <sp:Strict/>
             </wsp:Policy>
           </sp:Layout>
         </wsp:Policy>
       </sp:AsymmetricBinding>
       <sp:SignedSupportingTokens xmlns:sp="
http://schemas.xmlsoap.org/ws/2005/07/securitypolicy">                
         <wsp:Policy>
           <sp:UsernameToken sp:IncludeToken="
http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/Once"/>
         </wsp:Policy>                
       </sp:SignedSupportingTokens>
       <rampc:RampartConfig xmlns:rampc="
http://ws.apache.org/rampart/c/policy">
         <rampc:PasswordCallbackClass>
           /PATH/TO/CALLBACK_LIBRARY.SO
         </rampc:PasswordCallbackClass>
       </rampc:RampartConfig>
     </wsp:All>
   </wsp:ExactlyOne>
 </wsp:Policy>

 <operation name="add">
   <!--messageReceiver class="axis2_receivers" /-->
 </operation>
 <operation name="sub">
   <!--messageReceiver class="axis2_receivers" /-->
 </operation>
 <operation name="mul">
   <!--messageReceiver class="axis2_receivers" /-->
 </operation>
 <operation name="div">
   <!--messageReceiver class="axis2_receivers" /-->
 </operation>
</service>

For the value of this tag:

  <rampc:PasswordCallbackClass>
    /PATH/TO/CALLBACK_LIBRARY.SO
  </rampc:PasswordCallbackClass>

You just need to replace it with a callback library that will return the password for the username sent in the request.  In the WSO2 bundle a sample callback library can be found in rampartc/samples/callback.  That's what I used for this test.  More info for the rampart configuration of an axis2c service can be found here.

With that now configured I can move on to my Perl client.  I wanted to see the requests and responses that were being sent - figured it would be helpful to troubleshoot - so I used tcpmon.  tcpmon also comes bundled in the WSO2 package and once built and installed you can find it in $WSFC_HOME/bin/tools/tcpmon.

First thing I just wanted to see what would happen if I called the protected service with no credentials in my soap header.  Here's my simple Perl client with no security stuff added:

#!/usr/bin/perl -w

use SOAP::Lite;

my $service_call = SOAP::Lite
-> uri('http://ws.apache.org/axis2/services/math')
-> proxy('http://localhost:9099/axis2/services/math/add');

my $param1Element = SOAP::Data->name('param1')->value('1');

my $param2Element = SOAP::Data->name('param2')->value('2');

my $response = $service_call->add(
$param1Element
, $param2Element
);

print "The result is " . $response->dataof('//result')->value . "\n";

Pretty simple.  With this setup here's what my request looks like:

    <soap:Body>
        <param1 xsi:type="xsd:int">1</param1>
        <param2 xsi:type="xsd:int">2</param2>
      </add>
    </soap:Body>
  </soap:Envelope>

And here's the response:
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Header></soapenv:Header>
    <soapenv:Body>
      <soapenv:Fault>
        <faultcode>soapenv:Client</faultcode>
        <faultstring>Array list index out of bounds</faultstring>
      </soapenv:Fault>
    </soapenv:Body>
  </soapenv:Envelope>

Honestly I'm not too sure what that error message means.  I see it fairly often with this version of WSO2 (1.3) and I think it's probably a bug (maybe a subject for another post).  An excerpt from my HTTP server log is a little more helpful:

[Mon Nov 22 05:05:04 2010] [error] rampart_in_handler.c(106) [rampart][rampart_in_handler] SOAP header cannot be found.

Now we're getting somewhere.  Rampart expected some security info in the SOAP header and didn't find any.  Let's fix that.  Here's the new version of my perl script with the code added to build my security header.

#!/usr/bin/perl -w

use SOAP::Lite;

my $service_call = SOAP::Lite
-> uri('http://ws.apache.org/axis2/services/math')
-> proxy('http://localhost:9099/axis2/services/math/add');

my $param1Element = SOAP::Data->name('param1')->value('1');

my $param2Element = SOAP::Data->name('param2')->value('2');

my $username = 'alice';
my $password = 'password';

my $security=SOAP::Header->name(
"wsse:Security"
)->attr(
{'soap:mustUnderstand'=>1,'xmlns:wsse'=>'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'}
);


my $userToken =SOAP::Header->name(
"wsse:UsernameToken" => \SOAP::Header->value(
SOAP::Header->name(
'wsse:Username'
)->value(
$username
)->type(
''
)
, SOAP::Header->name(
'wsse:Password'
)->value(
$password
)->type(
''
)->attr(
{'Type'=>'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'}
)
)
)->attr({'xmlns:wsu'=>'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'});

my $response = $service_call->add(
$param1Element
, $param2Element
, $security->value(
\$userToken
)
);

print "The result is " . $response->dataof('//result')->value . "\n";

I certainly cannot take credit for this code.  I needed help and found it here.  The stuff in red is new.  It just manually builds the security tags to put in the SOAP header.  I like it that it's so easy to build tags and add them with SOAP::Lite, but it seems like there would be some built-in functions to do the WS-Security stuff.  Perhaps there is and I haven't found it yet.  Nevertheless, it does the trick.  With the new code added here's the request:

  <soap:Header>
    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1">
        <wsse:Username>alice</wsse:Username>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
      <param1 xsi:type="xsd:int">1</param1>
      <param2 xsi:type="xsd:int">2</param2>
    </add>
  </soap:Body>
</soap:Envelope>

And here's the response.  Apparently 1+2 does equal 3.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header>
    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
    </wsse:Security>
  </soapenv:Header>
  <soapenv:Body>
    <ns1:result xmlns:ns1="http://axis2/test/namespace1">3</ns1:result>
  </soapenv:Body>
</soapenv:Envelope>