Implementing WS-Security in a SOAP::Lite client

SOAP::Lite does not contain support for WS-Security, especially for the userToken profile as specified in http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf. The good news is, that there is a quite simple way to integrate it. This will be shown in the next lines.

The userToken profile requires additional header data to be sent. An example:

  <soap:Header>
    <wsse:Security 
        xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 
        xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
        soap:mustUnderstand="1">
      <wsse:UsernameToken>
        <wsse:Username>aUserName</wsse:Username>
        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
        >x6aFVLxxyv10N5P6D/XL4t3578A=</wsse:Password>
        <wsse:Nonce>YSBzZXF1ZW5jZSB2YWx1ZSBvZiA4OTA=</wsse:Nonce>
        <wsu:Created>2009-05-22T15:36:13Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>

To create data which get serialized into the soap header, one have to use SOAP::Header instead of SOAP::Data and pass it to the webservice call as an additional parameter, e.g.:

$proxy->getSimpleResult($data,SOAP::Header->new(...))

There is no much magic in creating the wsse:Security element, one issue should be mentioned:
The specification requires the client to provide a countermeasure for replay attacs. Beside a username and a (hashed) password, the client has to provide a timestamp and a random value called "Nonce". The client does not hash the password alone, but a concatenated string consisting of passwort, timestamp and nonce. One easy way to create a nonce is to use a sequence generator, e.g.:

sub create_generator {
    my ($name,$start_with) = @_;
    my $i = $start_with;
    return sub {  $name . ++$i; };
}

*default_nonce_generator = create_generator( "a value of ", int(1000*rand()) );

With some supporting subs, the wsse:Security element can be generated as follows:

use Time::Local;
use Digest::SHA1;
use MIME::Base64;

sub xml_quote {
    my ($value) = @_;
    $value =~ s/&/&amp;/;
    $value =~ s/</&lt;/;
    $value =~ s/>/&gt;/;
    $value;
}

sub _complex_type {
    my ($name,@childs) = @_;
    my $data = SOAP::Data->new( name => $name );
    $data->value( \SOAP::Data->value(@v));
    $data;
}

sub _typeless {
    my ($name,$value) = @_;
    my $data = SOAP::Data->new( name => $name );

    $value = xml_quote($value);

    $data->value( $value );
    $data->type( "" );
    $data;
}

sub timestamp {
    my ($sec,$min,$hour,$mday,$mon,$year,undef,undef,undef) = gmtime(time);
    $mon++;
    $year = $year + 1900;
    return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",$year,$mon,$mday,$hour,$min,$sec);
}

sub create_generator {
    my ($name,$start_with) = @_;
    my $i = $start_with;
    return sub {  $name . ++$i; };
}

*default_nonce_generator = create_generator( "a value of ", int(1000*rand()) );

sub ws_authen {
    my($username,$passwort,$nonce_generator) = @_;
    if(!defined($nonce_generator)) {
        $nonce_generator = \&default_nonce_generator;
    }
    my $nonce = $nonce_generator->();
    my $timestamp = timestamp();

    my $pwDigest =  Digest::SHA1::sha1( $nonce . $timestamp . $passwort );
    my $passwortHash = MIME::Base64::encode_base64($pwDigest,"");
    my $nonceHash = MIME::Base64::encode_base64($nonce,"");

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

    $auth->value( \SOAP::Data->value(
         _complex_type ( "wsse:UsernameToken",
            _typeless("wsse:Username",$username),
            _typeless("wsse:Password",$passwortHash)->attr({
                "Type" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
            }),
            _typeless("wsse:Nonce",$nonceHash),
            _typeless("wsu:Created",$timestamp),
        )
       )
    );
    $auth;
}

Finally, to add the WS-Security data to your call, just the result of a ws_authen() function call to parameter list:

$proxy->getSimpleResult($data,ws_authen->($username,$password))

Enjoy it!