Using SOAP PHP with NTLM Authentication

The SOAP-PHP extension does not handle NTLM Authentication used by IIS Server. So how can we solve this issue ? Well, by mixing some PHP modules :
- cURL : manage the connection throught NTLM Authentication
- Stream Functions : Create a NTLM Stream. PHP allows you to define or redefine a wrapper for a protocol (HTTP for instance), that means you can redefine functions such as fopen, fread, stat and so on for one protocol.
- NTMLSOAPClient : extends the object to send request trough cUrl


So this article we are going to create a stream object that open a NTML Wrapper with cURL and implements the basic functions require to make it work with the SOAPClient Object.

Documentations

You should consider to read the modules documentations if you want a better understanding about what's happening next :

Licence of the code

/*
* Copyright (c) 2008 Invest-In-France Agency http://www.invest-in-france.org
*
* Author : Thomas Rabaix
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

NTLMStream

Class to connect to the webservice

class NTLMStream {
private $path;
private $mode;
private $options;
private $opened_path;
private $buffer;
private $pos;

/**
* Open the stream
*
* @param unknown_type $path
* @param unknown_type $mode
* @param unknown_type $options
* @param unknown_type $opened_path
* @return unknown
*/
public function stream_open($path, $mode, $options, $opened_path) {
echo "[NTLMStream::stream_open] $path , mode=$mode \n";
$this->path = $path;
$this->mode = $mode;
$this->options = $options;
$this->opened_path = $opened_path;

$this->createBuffer($path);

return true;
}

/**
* Close the stream
*
*/
public function stream_close() {
echo "[NTLMStream::stream_close] \n";
curl_close($this->ch);
}

/**
* Read the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_read($count) {
echo "[NTLMStream::stream_read] $count \n";
if(strlen($this->buffer) == 0) {
return false;
}

$read = substr($this->buffer,$this->pos, $count);

$this->pos += $count;

return $read;
}
/**
* write the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_write($data) {
echo "[NTLMStream::stream_write] \n";
if(strlen($this->buffer) == 0) {
return false;
}
return true;
}


/**
*
* @return true if eof else false
*/
public function stream_eof() {
echo "[NTLMStream::stream_eof] ";

if($this->pos > strlen($this->buffer)) {
echo "true \n";
return true;
}

echo "false \n";
return false;
}

/**
* @return int the position of the current read pointer
*/
public function stream_tell() {
echo "[NTLMStream::stream_tell] \n";
return $this->pos;
}

/**
* Flush stream data
*/
public function stream_flush() {
echo "[NTLMStream::stream_flush] \n";
$this->buffer = null;
$this->pos = null;
}

/**
* Stat the file, return only the size of the buffer
*
* @return array stat information
*/
public function stream_stat() {
echo "[NTLMStream::stream_stat] \n";

$this->createBuffer($this->path);
$stat = array(
'size' => strlen($this->buffer),
);

return $stat;
}
/**
* Stat the url, return only the size of the buffer
*
* @return array stat information
*/
public function url_stat($path, $flags) {
echo "[NTLMStream::url_stat] \n";
$this->createBuffer($path);
$stat = array(
'size' => strlen($this->buffer),
);

return $stat;
}

/**
* Create the buffer by requesting the url through cURL
*
* @param unknown_type $path
*/
private function createBuffer($path) {
if($this->buffer) {
return;
}

echo "[NTLMStream::createBuffer] create buffer from : $path\n";
$this->ch = curl_init($path);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($this->ch, CURLOPT_USERPWD, $this->user.':'.$this->password);
echo $this->buffer = curl_exec($this->ch);

echo "[NTLMStream::createBuffer] buffer size : ".strlen($this->buffer)."bytes\n";
$this->pos = 0;

}
}

Now we have to create a class for your custom SOAP Provider

class MyServiceProviderNTLMStream extends NTLMStream {
protected $user = 'myuser';
protected $password = '*******';
}

Request the Webservice


$url = 'http://myIISServer.com/xmlservice?wsdl';

// we unregister the current HTTP wrapper
stream_wrapper_unregister('http');

// we register the new HTTP wrapper
stream_wrapper_register('http', 'MyServiceProviderNTLMStream') or die("Failed to register protocol");

// so now all request to a http page will be done by MyServiceProviderNTLMStream.
// ok now, let's request the wsdl file
// if everything works fine, you should see the content of the wsdl file
$client = new SoapClient($url, $options);

// but this will failed
$client->mySoapFunction();

// restore the original http protocole
stream_wrapper_restore('http');

The unexpected issue

The unexpected issue is that the SOAP object does not use the new HTTP Stream to send the query to the server ! So the request is not done through NTLM Authentication. Let's fix that by reimplement the SOAPClient::__doRequest method. The __doRequest method is the low level method that send the request to the webservice.

class NTLMSoapClient extends SoapClient {
function __doRequest($request, $location, $action, $version) {

$headers = array(
'Method: POST',
'Connection: Keep-Alive',
'User-Agent: PHP-SOAP-CURL',
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "'.$action.'"',
);

$this->__last_request_headers = $headers;
$ch = curl_init($location);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true );
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($ch, CURLOPT_USERPWD, $this->user.':'.$this->password);
$response = curl_exec($ch);

return $response;
}

function __getLastRequestHeaders() {
return implode("\n", $this->__last_request_headers)."\n";
}
}

// Authentification parameter
class MyServiceNTLMSoapClient extends NTLMSoapClient {
protected $user = 'myuser';
protected $password = '*******';
}

Request the webservice II


$url = 'http://myIISServer.com/xmlservice?wsdl';

// we unregister the current HTTP wrapper
stream_wrapper_unregister('http');

// we register the new HTTP wrapper
stream_wrapper_register('http', 'MyServiceProviderNTLMStream') or die("Failed to register protocol");

// so now all request to a http page will be done by MyServiceProviderNTLMStream.
// ok now, let's request the wsdl file
// if everything works fine, you should see the content of the wsdl file
$client = new MyServiceNTLMSoapClient($url, $options);

// should display your reply
echo $client->mySoapFunction();

// restore the original http protocole
stream_wrapper_restore('http');

Conclusion

  • The stream_wrapper_register PHP feature is a well-hidden feature and very useful to extend missing features.
  • Due to a bug in the SOAPClient, the stream wrapper does not work in 'write' mode
  • The code needs some cleanup before it can be used.
  • This code is not optimized for large reply and binary information
  • This code has not been tested on production server, and uses this code at your own risk.

Licence of this document

Creative Commons License
This work is licensed under a Creative Commons Attribution 2.0 France License.

Comments

Jamie Talbot
5 days after
Nice work, I've been waiting for this in the PHP core, but this will hopefully work as an interim measure! One question though; how come you need to register the stream wrapper, if you're making a direct cURL request in the SoapClient? It seems redundant (not having worked through the code yet). Cheers, Jamie
Thomas Rabaix
6 days after
@Jamie : I was looking for the solution which is the more transparent. Due to the bug, I have created the NTMLSoapClient Object, I will move the un/registering code into the NTMLSoapClient::__construct.

This solution required to have curl with ssl and works like a charm (but still required testing).
Sam McDonald
6 days after
Thanks a lot. This is very useful I am trying to get web services working with a company that doesn't help you out at all if you are using php, and this was a big help.

When you subclass NTMLSoapClient, you spell it incorrectly, but other than that everything worked great. You just helped me out a ton!
kj33p
3 months after
Great work, i am connecting to a https wsdl which is using a self-signed certificate. I can't find anything regarding this when constructing the soap client. I need to set the "CURLOPT_SSL_VERIFYPEER" curl option to false. When can i find the code for SoapClient->__construct to duplicate and set the right curl opts? Ideas?
Graeme Canivet
7 months after
This is awesome! The alternative was working with MS .NET, so I'm very glad I found this. Just another note, I had to fix some $ch references in createBuffer() which should be $this->ch.
Norbert Gil
8 months after
Hi Thomas, Thank you for those explanations. Do you mind if i ask you to send me a copy of your files? Because I don't really get, when you use this, how you call the soap client, which file should be included in which one and so on. To say clearer, I don't get the structure of those files. So to see a "true" example would help me a lot! Best regards!
Tom
9 months after
Hi Thomas, thanks for your great work! I am running into problems with your code because I try to access a https url of a wsdl that needs NTLM. I always get "Unauthorized error". What do you think? Thanks a lot in advance.
Thomas Rabaix
9 months after
@tom : you should check that your curl support ssl and your login/pass are valid.
NR
10 months after
Little error in your source:

class NTMLSoapClient extends SoapClient {

NTLM instead of NTML

Add comment











Tags

Promouvoir et soutenir le logiciel libre