I want to write a SOAP server. I want to authenticate clients somehow - username and password are a good start. I want every request to be authenticated, and a Basic Authentication Realm will work just fine.
Why make every request authenticate? Because the web is a stateless protocol. When you "log in" to a website, you really are just requesting a cookie. This cookie is a magic number that is stored on the server and means "your session". When you load another page on that same site, the browsers sends that cookie along as well. But the server still has to authenticate the validity of that cookie because it will be sent on the next page load, whether it's made in 2 seconds or 2 years. Basic Authentication is similar, except it's built-in to the browser. This has downsides though, the biggest is that your browser asks you to log in so the website developer can't make a fancy branded prompt. But I digress...
The second issue I will run into is that I want to be able to retrieve those credentials from the class methods being called. There are a number of reasons to do this: perhaps different clients will get different responses, or perhaps I want to log some AAA data. This is where I need to search back to 2004.
So let's revisit with some source code.
The SOAP server
- soapDaemon.pl
use strict; use SOAP::Transport::HTTP +'trace'; # don't want to die on 'Broken pipe' or Ctrl-C #$SIG{PIPE} = $SIG{INT} = 'IGNORE'; $SIG{PIPE} = 'IGNORE'; #my $daemon = SOAP::Transport::HTTP::Daemon my $daemon = BasicAuthDaemon -> new (LocalPort => 8000, Reuse => 1) -> dispatch_to('myClass') ; print "Connect to SOAP server at ", $daemon->url, "\n"; $daemon->handle;
The modified HTTP::Daemon class
- soapDaemon.pl
package BasicAuthDaemon; use strict; use warnings; use MIME::Base64; use SOAP::Lite +'trace'; 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, $creds) = split /\s+/, $r->headers->authorization; my ($user, $pass) = split /:/, decode_base64( $creds ) if( $type eq 'Basic' ); print "type: [$type], user:[$user], pass:[$pass]\n"; if( $user eq 'user' and $pass eq 'password' ) { $self->{'auth'} = "123123123"; SOAP::Transport::HTTP::Server::handle $self; #$self->SUPER::handle; } else { $self->response( $self->make_fault( $SOAP::Constants::FAULT_CLIENT, 'Authentication required', 'Give authentication credentials for Basic Realm security' )); } $c->send_response( $self->response ); } # replaced ->close, thanks to Sean Meisner <Sean.Meisner@VerizonWireless.com> # shutdown() doesn't work on AIX. close() is used in this case. Thanks to Jos Clijmans <jos.clijmans@recyfin.be> $c->can('shutdown') ? $c->shutdown(2) : $c->close(); $c->close; } } 1;
This code is in the same file as the server code above. I actually have this as the top of the file but it takes a bit more explaining. Firstly, this is still just test code because you're unlikely to use HTTP::Daemon in production. I think this actually gets easier in production because the handle() method doesn't have a service loop where requests have to be processed. See here for some clarification.
However, what I've done is gone into the file perl5/SOAP/Transport/HTTP.pm and found the handle() method within the SOAP::Transport::HTTP::Daemon package, and copied it into my code. I've left the original comments and expanded formatting to show that this is a copy-paste and not my own code. My additions are bold and removals are italic.
My added logic takes the HTTP request and references into the authorization headers that are sent for the Basic Authentication mechanism. You can use a similar routine to extract and verify cookie data, etc.
If the username and password match, I jam a value into $self, which is a SOAP context. I'm simply creating a hash called 'auth', which could be enhanced. Is there a 'stash' in this architecture? I don't think so... Should I call it something more unique like 'myAppAuth'? Probably. But this is where we begin.
Aside from adding my custom logic, one more modification was necessary: remove the call to $self->SUPER::handle. This is necessary because our superclass has the same event loop and will just hang waiting for another client. Instead we need to call the superclass of our superclass, which is SOAP::Transport::HTTP::Server. I pass $self as the first argument because this is how perl seems to do OO things, and it works nicely.
Finally, if the credentials don't match, I return a SOAP error.
The client (in perl)
- soapClient.pl
use strict; use SOAP::Lite +'trace'; my $soapClient = new SOAP::Lite uri => 'http://example.com/myClass', proxy => 'http://user:password@localhost:8000/', ; my $result = $soapClient->hi( 'this is how the world ends', 'kabaam' ); unless ($result->fault) { print "\nresult: [" . $result->result . "]\n\n"; } else { print join ' --=-- ', $result->faultcode, $result->faultstring, $result->faultdetail, "\n"; }
This is the same client as before. Again, the uri is the method being called and the host name appears to be ignored. The proxy is the actual web server, and is where we place the username and password.
The client (in PHP)
- soapClient.php
$client = new SoapClient(null, array('location' => "http://user:somepass@localhost:8000/", 'uri' => "http://test-uri/myClass", 'login' => "some_name", 'password' => "some_password", )); $res = $client->hi( 'from php!' ); print "got: $res\n";
Here's the same SOAP client written in PHP for comparison. Placing the username and password in the URL doesn't work in PHP, so these have to be explicitly added. Note again that the server doesn't seem to care about the server portion of the URI. On error, PHP throws an exception.
The SOAP class
- myClass.pm
package myClass; use strict; use vars qw(@ISA); @ISA = qw(Exporter SOAP::Server::Parameters); use SOAP::Lite;# +'trace'; sub hi { my $evp = pop; my $context = $evp->context; my ($name, @args) = @_; print "[$name] got arguments: [@args]\n"; return "hello, world, auth user=$context->{'auth'}"; } sub bye { return "goodbye, cruel world"; } 1;
First, this code must exist in an external file. I surmised that this could be placed in the same file as the daemon code, and it does work for how I was using it yesterday. However, this version is a subclass of SOAP::Server::Parameters (doesn't that seem like how it should work?) For reasons I don't understand, the subclassing doesn't work if this code is in the same file, even if I place it in the BEGIN{} block. If anyone knows why, I'd love to hear an explanation.
So now that we're a subclass of SOAP::Server::Parameters, we automatically get a new parameter added as the last argument to every function call we make. We pop this value off the end of the parameter stack, saving it as our environment pointer. This is really a SOAP::SOM object but what I want is the original SOAP context. Fortunately, this is easy to access through the ->context method.
Once I get the context, the 'auth' hash I created is ready available, and I return it in the call to hi() for verification. Fortunately, it works like - and appears to be slightly - magic.