Para conseguires executar pedidos SOAP em PHP, tens de instalar a extensão PHP-SOAP. Contudo, a classe SoapClient, fornecida por essa extensão, não permite autenticação NTLM.

Antes de avançar é preciso referir que a informação que aqui apresento (e é apresentada em praticamente todos os restantes artigos por essa Internet fora), deve-se ao facto de Thomas Rabaix ter escrito um artigo em março de 2008 sobre o assunto. Fica aqui o meu agradecimento ao seu contributo e à partilha do código. Caso ainda não tenhas lido o artigo, sugiro que o faças.

Porque é que o SoapClient não serve?

É importante perceber porque é que o SoapClient não tem suporte para autenticação NTLM. É tudo uma questão de protocolos e o protocolo NTLM tem um fluxo de mensagens bastante diferente do protocolo HTTP (que é usado para fazer pedidos SOAP simples).

Tipicamente quando é feito um pedido SOAP, o cliente envia uma mensagem ao servidor e o servidor há de responder com uma mensagem que contenha os dados pedidos. No caso do protocolo NTLM a situação é um pouco mais complexa e são necessários três pedidos (dois da autenticação e um para obter a resposta do método). A figura representa o fluxo da autenticação NTLM.

Consumir Web Service SOAP com autenticação NTLM em PHP images/16-consume-soap-web-services-with-ntlm-authentication-in-php/266-basic-ntlm-authentication-flow.png

Fonte: Microsoft MSDN

Solução

Uma vez que o SoapClient não tem suporte nativo para esse protocolo, o que vamos fazer é:

  1. Criar uma Stream NTLM, que seja usada no pedido em vez do protocolo HTTP.
  2. Usar o cURL para redefinir a forma como os pedidos são feitos no SoapClient

Pegando no código do Thomas Rabaix, deves criar um NTLM Http Stream Wrapper que é uma classe com o seguinte código:

<?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;
    }
}

Depois disto, para conseguires consumir Web Services com autenticação NTLM deves criar o teu próprio SoapClient, que extende a classe nativa do PHP, de forma a usares a stream definida anteriormente. Além disso deves fazer override ao método __doRequest(), por forma a passares os dados do utilizador.

Portanto, deves criar uma classe com o código abaixo.

<?php

namespace Geekalicious;

use Exception;
use SoapClient;

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

    /**
     * @param mixed $url Endereço do WSDL (exemplo: http://dominio.tld/webservice.aspx?wsdl)
     * @param array $data Array de dados a usar para criar a instância
     * @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'];

            // Remover registo da stream HTTP
            stream_wrapper_unregister('http');

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

            // Cria instancia do SoapClient nativo
            parent::__construct($url, $data);

            // Voltar a definir a Stream HTTP por defeito
            stream_wrapper_restore('http');
        }
    }

    /**
    * Efetua um pedido cURL e envia os dados do utilizador no pedido
    * @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;
    }
}

Agora, sempre que quiseres consumir um Web Service com autenticação NTLM, deve fazer a chamada com o código:

<?php

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

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

    $parametros = [
        'campo1' => 'valor',
        'campo2' => 'valor2'
    ];

    try {
        $resposta = $soapCliente->NomeDoMetodo($parametros);
    } catch (\SoapFault $ex) {
        // Tratar excepção Soap aqui
    }

Usar packages já existentes com Composer

Se usas uma framework PHP que suporte o Composer (por exemplo, Laravel), existe uma package que te permite fazer isto out-of-the-box: https://github.com/mlabrum/NTLMSoap. O projeto não parece ter grande manutenção, mas existem melhorias nos forks feitos e a verdade é que a package original resolve o problema e ainda permite registar os pedidos feitos aos Web Services.

Fazer debug com Fiddler

Se usas Windows o Fiddler é uma excelente ferramenta de debug, que te permite monitorizar os dados enviados e recebidos a cada pedido ao Web Service. Para ver essa informação no Fiddler tens de adicionar a opção ao objeto cURL, no método __doRequest():

// Esta opção permite monitorizar os pedidos ao Web Service no cURL
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:8888');