Consume SOAP Web Services with NTLM authentication in PHP

Published at 26-07-2015 23:12 | Created by Luís Cruz | Category: PHP
Clique aqui para ver a versão Portuguesa

To be able to consume SOAP web services with PHP you need to install the PHP-SOAP extension. However, the SoapClient class provided by that extension does not allow NTLM authentication.

Before we go into the solution I would like to thank Thomas Rabaix for his blog post on March '08 about the subject. Indeed, his post is the real source for all the solutions online. If you hadn't read the article I suggest you to do so.

 

Why SoapClient doesn't work?

You should know why there's no NTLM authentication support available on SoapClient. It all comes to protocol definitions and NTLM protocol has a message flow a bit different from HTTP (which is used in simple SOAP requests).

Usually when a SOAP request is made, the client sends a message (SOAP envelope) to the server and the server responds with another message that contains the method's response data. The problem with NTLM authentication is that it needs three requests to get the server's response (two for authentication purposes and another to get the method's response). The following image represents the NTLM authentication flow.

NTLM authentication flow

Source: Microsoft MSDN

Solution

Since there's no native support for NTLM in SoapClient here's how we solve the issue:

  1. Create a NTLM stream to be used instead of HTTP.
  2. Use cURL to replace the default SoapClient request and provide authentication parameters.

So you need to create the following NTLM Stream Wrapper which is a class with the following code (taken from Thomas Rabaix's article):

<?php
/*
* 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.
*/

namespace Geekalicious;

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 you need to create your own SoapClient implementation that extends PHP's native class in order to use the previous defined class. Besides that, you need to override __doRequest() method in order to use the NTLM authentication parameters.

Here's what you need to create.

<?php

namespace Geekalicious;

use Exception;
use SoapClient;

class MySoapClient extends SoapClient
{
    private $options = [];

    /**
     * @param mixed $url WSDL url (eg: http://dominio.tld/webservice.aspx?wsdl)
     * @param array $data Array to be used to create an instance of SoapClient. Can take the same parameters as the native class
     * @throws Exception
     */
    public function __construct($url, $data)
    {
        $this->options = $data;

		if (empty($data['ntlm_username']) && empty($data['ntlm_password'])) {
			parent::__construct($url, $data);
		} else {
			$this->use_ntlm = true;
			NTLMStream::$user = $data['ntlm_username'];
			NTLMStream::$password = $data['ntlm_password'];

			// Remove HTTP stream registry
			stream_wrapper_unregister('http');

			// Register our defined NTLM stream
			if (!stream_wrapper_register('http', '\\Geekalicious\\NTLMStream')) {
				throw new Exception("Unable to register HTTP Handler");
			}

			// Create an instance of SoapClient
			parent::__construct($url, $data);

			// Since our instance is using the defined NTLM stream,
			// you now need to reset the stream wrapper to use the default HTTP.
			// This way you're not messing with the rest of your application or dependencies.
			stream_wrapper_restore('http');
		}
	}

	/**
	* Create a cURL request and return the method's response
	* @param string $request
	* @param string $location
	* @param string $action
	* @param int $version
	* @param int $one_way
	* @see SoapClient::__doRequest()
	* @return mixed
	*/
	public function __doRequest($request, $location, $action, $version, $one_way = 0)
	{
		$this->__last_request = $request;

		$ch = curl_init($location);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		curl_setopt($ch, CURLOPT_HTTPHEADER, [
			'Method: POST',
			'User-Agent: PHP-SOAP-CURL',
			'Content-Type: text/xml; charset=utf-8',
			'SOAPAction: "' . $action . '"',
		]);

		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
		if (!empty($this->options['ntlm_username']) && !empty($this->options['ntlm_password'])) {
			curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
			curl_setopt($ch, CURLOPT_USERPWD, $this->options['ntlm_username'] . ':' . $this->options['ntlm_password']);
		}
		$response = curl_exec($ch);

		return $response;
	}
}

Now, whenever you need to consume a NTLM authenticated Web Service, you should call the method with the following code:

<?php

	$wsdlUrl = 'http://dominio.tld/webservice.aspx?wsdl';
	$options = [
		'ntlm_username' => 'domain\username',
		'ntlm_password' => 'password'
	];

	$soapCliente = new Geekalicious\MySoapClient($wsdlUrl, $options);

	$parametros = [
		'field1' => 'value1',
		'field2' => 'value2'
	];

	try {
		$response = $soapCliente->YourMethodName($parametros);
	} catch (\SoapFault $ex) {
		// Treat SOAP exception here
	}

Using existing Composer packages

If you're using a PHP framework that supports Composer (such as Laravel), there's a package on GitHub that allows you to do this out-of-the-box: https://github.com/mlabrum/NTLMSoap. The project doesn't seem to be maintained but it really solves the issue. Take a look at the project's forks if you're looking for improvements.

 

Debug web service requests with Fiddler

If you're using Windows, Fiddler is an excellent tool to debug web service requests because you can monitor the sent and received data. In order to view all requests for a given NTLM authenticated web service you need to add a specific option to your cURL's object in __doRequest() method:

// This option allows you to monitor Web Service requests in Fiddler
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:8888');