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
woffi
11 months after
Hi! It's a great work!
But anyone tell me please why we need to unregister (and then register/restore) the stream_wrapper?
Strawp
about 1 year after
Brilliant! Requires a little bit of tweaking to get it working but it works in the end!

I will be making a wrapper class for Sharepoint based around this.
Strawp
about 1 year after
OK, I'm guessing this is some SOAP issue, but I'm not getting the data back parsed - it's just a big string of everything inside the result tags of the response (i.e. unparsed XML).

It's driving me nuts. Any ideas?
Kevin
about 1 year after
So .... if I wanted to use this code but needed to pass in some username and password dynamically how would I go about doing that? I tried setting the following, but it doesn't work:

protected $user = $ntusername;

Any ideas?
adge
about 1 year after
hoping you can provide a complete, working example? I'm having a lot of trouble getting any sort of response - even an error message to tell me something is wrong. Appreciate any help or guidance.

Thanks!
Mikey
about 1 year after
Fantastic! A brilliant solution that works flawlessly (for me anyway).
Michael
over 2 years after
Seems to be a very nice solution! I have a huge problem though...

Basically I simply have to call a method defined by the wsdl: Retrieve. The expected XML contains 3 elements of which the 3rd is empty but has an attribute. Like:
foo
bar


If we forget about arg3 for a moment I assume this would be fine:

$param = array('arg1' => 'foo', 'arg2' => 2);
$result = $client->Retrieve($param);

But how do I deal with arg3?? I have tried with the '_' trick (the '_' key containing the value - like:
array(..., 'arg3' => array('_' => '', 'myattrib' => 'test'))

Nothing seems to work though... It is pretty frustrating to be so close to the goal - but cannot find any way to get to it =/
Cast
over 2 years after
Thomas Rabaix, you are my hero.
Patrick Logé
over 2 years after
https://bugzilla.redhat.com/show_bug.cgi?id=603783

Kamil Dudka 2010-06-14 11:30:08 EDT

NTLM support is disabled since curl's migration from OpenSSL to NSS. If you
build the upstream libcurl on top of NSS, it doesn't support NTLM either. I'll
check if it is intentionally or not.
Nupsi
over 2 years after
I get always the error message that Soap are not able to bind to service. I used echo $this->buffer = curl_exec($this->ch); for debbuging and i can see that cUrl has catched the WSDL. Any idea for solving that?
Petr
over 4 years after
Hi Thomas,
Your solution working perfect. I am using PHP 5.3 with IIS7 which has enabled NTLM authentication. My PHP application is connecting to asmx web service on the same http server.
At the moment when I’am trying to connect asmx webservice I have already authenticated header and I don’t want sending CURLOPT_USERPWD. Does any possibility to use current authenticated header instead of sending curl_setopt($ch, CURLOPT_USERPWD, $this->user.':'.$this->password);

Thanks
Pharmf825
over 4 years after
Hello! ekdbdca interesting ekdbdca site! I'm really like it! Very, very ekdbdca good!
Pharmc312
over 4 years after
Very nice site! cheap viagra
Pharmf373
over 4 years after
Very nice site! cheap cialis http://aixopey.com/qqvtst/4.html
Pharme31
over 4 years after
Very nice site!

Add comment











A la recherche de projets symfony ?

Tags

Last posts