Wednesday, August 14, 2013

Create a SOAP::Lite Server that uses Basic Authentication for password verification

Today was a rough day. I'm writing a SOAP service in perl. The basics required to make this work are fairly straightforward, but, as usual, the documentation and I can't seem to understand one another very well. As far as I can tell, this recipe exists nowhere on the internet.

Let me start with the basic example. This is what a standalone server looks like:
    use strict;
    use SOAP::Transport::HTTP;
    # don't want to die on 'Broken pipe'
    $SIG{PIPE} = 'IGNORE';
    my $daemon = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 8000, Reuse => 1)
        -> dispatch_to('class::method')
        ;
    print "Contact to SOAP server at ", $daemon->url, "\n";
    $daemon->handle;
How much simpler can this get? We use the required SOAP module, setup a signal handler, and create an HTTP daemon. We set it to run on an unprivileged port (8000) and setup socket reuse. It has a single method that can be called from the 'class' package. We print out the URL and start handling requests.

If you're really just starting out, this should work to define the 'class::method' handler, for reference:
    package class;
    use strict;
    sub method { return 'Hello, world!' };
Go ahead and put it at the bottom of the file with the daemon code.



Now, to test this service you also need to write a SOAP client. Here's that code:
    use strict;
    use SOAP::Lite;
    my $soapClient = new SOAP::Lite
        uri => 'http://example.com/class',
        proxy => 'http://user:password@localhost:8000/',
        ;
    my $result = $soapClient->method( 'some_args' );
    unless ($result->fault) {
        print "\nresult: [" . $result->result . "]\n\n";
    } else {
        print join ' ** ',
            $result->faultcode,
            $result->faultstring,
            $result->faultdetail, "\n";
    }
It's a bit longer because of the error checking, but still fairly straightforward. We use the SOAP module, create a client, and call the method. Then we either print the result or the fault information. There are a couple things to note in here:
  1. The 'uri' parameter can contain any hostname. The only thing the server seems to care about is the name 'class'. This identifies what code is going to be dispatched.
  2. The name 'class' in the client 'url' must match the dispatch_to parameter in the server.
  3. The 'proxy' is the actual URL the request will be sent to. It can be http, https, mailto:, etc.
  4. We have added username and password credentials to the URL. This is important later.
Now, running this should Just Work. $soapClient becomes a remote representation of 'class', and we can call method() as though it were a local function. Of course it's not a local function and the web is stateless, so each request also passes that username and password we want to use to authenticate.

Here's what needs to happen on the server to receive and process the username and password. At the top of the file with the server code, add this:
    # subclass the default soap server daemon to handle authenticated requests
    package BasicAuthDaemon;
    use strict;
    use MIME::Base64;
    use SOAP::Transport::HTTP ();
    our @ISA= 'SOAP::Transport::HTTP::Daemon';
    sub handle {
        my $self = shift->new;
        while ( my $c = $self->accept ) {
            while ( my $r = $c->get_request ) {
                $self->request($r);
                my ($type, $crypt) = split /\s+/, $r->headers->authorization;
                my ($user, $pass) = split /:/, decode_base64( $crypt )
                    if( $type eq 'Basic' );
                print "user:[$user], pass:[$pass]\n";
                if( $user eq 'user' and $pass eq 'pass' ) {
                    #$self->SUPER::handle;
                    SOAP::Transport::HTTP::Server::handle $self;
                } else {
                    $self->response( $self->make_fault(
                        $SOAP::Constants::FAULT_CLIENT, 'Authentication required',
                        'Give authentication credentials for Basic Realm security'
                    ));
                }
                $c->send_response( $self->response );
            }
            $c->can('shutdown')
                ? $c->shutdown(2)
                : $c->close();
            $c->close;
        }
    }
What we're doing here is taking the code that can be found around line 688 of SOAP/Transport/HTTP.pm (in the SOAP::Transport::HTTP::Daemon package) and copying it into the 'BasicAuthPackage' that we are defining here. We use SOAP::Transport::HTTP and declare ourselves to be of the same type of the package we are subclassing.

Assuming the code is still legible despite the formatting, the code in blue that we added serves to extract the authentication data from the request headers, which looks like "Basic BASE64DATA==". We call the BASE64DATA "crypt" in the code but there's nothing secure about this - be sure to send credentials over SSL in production. We then decode the Base64 data to get the "user:password" string that was passed before the '@' in the client's URL. It is up to the reader to expand the code to do proper database lookups for production release.

However, do note the commented code in red: the call to $self->SUPER::handle will hang if left inline. Instead, I've replaced this with an explicit call to the handle method for HTTP::Server, which appears the be the super class for Transport::HTTP::Daemon. I'm also not an expert on SOAP so the make_fault may not be technically correct but it's working for now.

To complete the transition, we then simply redefine our daemon to use our derived class:

    #my $daemon = SOAP::Transport::HTTP::Daemon
    my $daemon = BasicAuthDaemon


No comments:

Post a Comment

Some HTML tags are accepted, SomeLink, bold and italic seem to be it.