how to implement ws-security using php to soap object - php

class WSSoapClient extends SoapClient {
private $username;
private $password;
/*Generates de WSSecurity header*/
private function wssecurity_header() {
/* The timestamp. The computer must be on time or the server you are
* connecting may reject the password digest for security.
*/
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
/* A random word. The use of rand() may repeat the word if the server is
* very loaded.
*/
$nonce = mt_rand();
/* This is the right way to create the password digest. Using the
* password directly may work also, but it's not secure to transmit it
* without encryption. And anyway, at least with axis+wss4j, the nonce
* and timestamp are mandatory anyway.
*/
$passdigest = base64_encode(
pack('H*',
sha1(
pack('H*', $nonce) . pack('a*',$timestamp).
pack('a*',$this->password))));
$auth='
<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">
<wsse:UsernameToken wsu:Id=\"UsernameToken-2\" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">
<wsse:Username>'.$username.'</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">'.$password.'</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
<wsa:Action>http://www.kbb.com/2011/01/25/VehicleInformationService/IVehicleInformationService/GetYears</wsa:Action>
';
/* XSD_ANYXML (or 147) is the code to add xml directly into a SoapVar.
* Using other codes such as SOAP_ENC, it's really difficult to set the
* correct namespace for the variables, so the axis server rejects the
* xml.
*/
$authvalues = new SoapVar($auth,XSD_ANYXML);
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-".
"200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues,
true);
return $header;
}
/* It's necessary to call it if you want to set a different user and
* password
*/
public function __setUsernameToken($username, $password) {
$this->username = $username;
$this->password = $password;
}
/* Overwrites the original method adding the security header. As you can
* see, if you want to add more headers, the method needs to be modifyed
*/
public function __soapCall($function_name, $arguments, $options=null,
$input_headers=null, $output_headers=null) {
$result = parent::__soapCall($function_name, $arguments, $options,
$this->wssecurity_header());
return $result;
}
}
I am trying to use this but I am getting the following error:
Fatal error: Uncaught SoapFault exception: [HTTP] Cannot process the message because the content type 'text/xml; charset=utf-8' was not the expected type 'application/soap+xml; charset=utf-8'
Please tell me how can I set the content type using SOAP object.

If somebody needs answer for the same question, the answer is simple. You need to use SOAP version 1.2 to pass Content-Type: application/soap+xml
$soapClient = new SoapClient('http://example.com/wsdl.wsdl',array(
'soap_version' => SOAP_1_2,
));
But, you must be careful, because it also adds action: youraction to Content-Type. For example:
Content-Type: application/soap+xml; charset=utf-8; action="http://example.com/path/to/your/action"

Related

Testing DocuSign webhooks with a connect key

I am using DocuSign Connect to retrieve webhooks from DocuSign and digest them within my Larave; application. Here is the basic idea.
<?php
namespace App\Http\Controllers;
use App\Http\Middleware\VerifyDocusignWebhookSignature;
use App\Mail\PaymentRequired;
use App\Models\PaymentAttempt;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class DocusignWebhookController extends Controller
{
/**
* Create a new controller instance.
* If a DocuSign Connect key is preset, validate the request.
*/
public function __construct()
{
$this->gocardlessTabs = ['GoCardless Agreement Number', 'GoCardless Amount', 'GoCardless Centre'];
$this->assumedCustomer = 2;
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
}
/**
* Handle an incoming DocuSign webhook.
*/
public function handleWebhook(Request $request)
{
$payload = json_decode($request->getContent(), true);
$shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);
if ($shouldProcessWebhook) {
switch ($payload['status']) {
case 'sent':
return $this->handleSentEnvelopeStatus($payload);
break;
case 'completed':
return $this->handleCompletedEnvelopeStatus($payload);
break;
case 'voided':
// ...
break;
default:
}
}
}
}
The logic itself works fine but if you look here:
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
If I specify a connect key I run some middleware to verify the webhook came from DocuSign.
The class to verify the signature came from DocuSign and looks like this:
<?php
namespace App\DocuSign;
/**
* This class is used to validate HMAC keys sent from DocuSign webhooks.
* For more information see: https://developers.docusign.com/platform/webhooks/connect/hmac/
*
* Class taken from: https://developers.docusign.com/platform/webhooks/connect/validate/
*
* Sample headers
* [X-Authorization-Digest, HMACSHA256]
* [X-DocuSign-AccountId, caefc2a3-xxxx-xxxx-xxxx-073c9681515f]
* [X-DocuSign-Signature-1, DfV+OtRSnsuy.....NLXUyTfY=]
* [X-DocuSign-Signature-2, CL9zR6MI/yUa.....O09tpBhk=]
*/
class HmacVerifier
{
/**
* Compute a hmac hash from the given payload.
*
* Useful reference: https://www.php.net/manual/en/function.hash-hmac.php
* NOTE: Currently DocuSign only supports SHA256.
*
* #param string $secret
* #param string $payload
*/
public static function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* #param string $secret: the secret known only by our application
* #param string $payload: the payload received from the webhook
* #param string $verify: the string we want to verify in the request header
*/
public static function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
Now, in order to test this locally I've written a test but whenever I run it, the middleware tells me the webhook isn't valid.
Here is my test class
<?php
namespace Tests\Feature\Http\Middleware;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class VerifyDocusignWebhookSignatureTest extends TestCase
{
use RefreshDatabase, WithFaker;
public function setUp(): void
{
parent::setUp();
config(['docusign.connect_key' => 'probably-best-not-put-on-stack-overflow']);
$this->docusignConnectKey = config('docusign.connect_key');
}
/**
* Given a JSON payload, can we parse it and do what we need to do?
*
* #test
*/
public function it_can_retrieve_a_webhook_with_a_connect_key()
{
Mail::fake();
$payload = '{"status":"sent","documentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents","recipientsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/recipients","attachmentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/attachments","envelopeUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514","emailSubject":"Please DocuSign: newflex doc test.docx","envelopeId":"2ba67e2f-0db6-46af-865a-e217c9a1c514","signingLocation":"online","customFieldsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/custom_fields","notificationUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/notification","enableWetSign":"true","allowMarkup":"false","allowReassign":"true","createdDateTime":"2022-02-14T11:36:01.18Z","lastModifiedDateTime":"2022-02-14T11:37:48.633Z","initialSentDateTime":"2022-02-14T11:37:49.477Z","sentDateTime":"2022-02-14T11:37:49.477Z","statusChangedDateTime":"2022-02-14T11:37:49.477Z","documentsCombinedUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/combined","certificateUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/certificate","templatesUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/templates","expireEnabled":"true","expireDateTime":"2022-06-14T11:37:49.477Z","expireAfter":"120","sender":{"userName":"Newable eSignature","userId":"f947420b-6897-4f29-80b3-4deeaf73a3c5","accountId":"366e9845-963a-41dd-9061-04f61c921f28","email":"e-signature#newable.co.uk"},"recipients":{"signers":[{"tabs":{"textTabs":[{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Amount","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"319","yPosition":"84","width":"84","height":"22","tabId":"207f970c-4d3c-4d0c-be6b-1f3aeecf5f95","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Centre","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"324","yPosition":"144","width":"84","height":"22","tabId":"f6919e94-d4b7-4ef4-982d-3fc6c16024ab","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Agreement Number","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"332","yPosition":"200","width":"84","height":"22","tabId":"9495a53c-1f5e-42a5-beec-9abcf77b4387","tabType":"text"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse","firstName":"","lastName":"","email":"Jesse.Orange#newable.co.uk","recipientId":"56041698","recipientIdGuid":"246ce44f-0c11-4632-ac24-97f31911594e","requireIdLookup":"false","userId":"b23ada8e-577e-4517-b0fa-e6d8fd440f21","routingOrder":"1","note":"","status":"sent","completedCount":"0","deliveryMethod":"email","totalTabCount":"3","recipientType":"signer"},{"tabs":{"signHereTabs":[{"stampType":"signature","name":"SignHere","tabLabel":"Signature 7ac0c7c8-f838-4674-9e37-10a0df2f81c1","scaleValue":"1","optional":"false","documentId":"1","recipientId":"38774161","pageNumber":"1","xPosition":"161","yPosition":"275","tabId":"371bc702-1a91-4b71-8c77-a2e7abe3210e","tabType":"signhere"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse Orange","firstName":"","lastName":"","email":"jesseorange360#gmail.com","recipientId":"38774161","recipientIdGuid":"844f781c-1516-4a5a-821a-9d8fb2319369","requireIdLookup":"false","userId":"f544f7ff-91bb-4175-894e-b42ce736f273","routingOrder":"2","note":"","status":"created","completedCount":"0","deliveryMethod":"email","totalTabCount":"1","recipientType":"signer"}],"agents":[],"editors":[],"intermediaries":[],"carbonCopies":[],"certifiedDeliveries":[],"inPersonSigners":[],"seals":[],"witnesses":[],"notaries":[],"recipientCount":"2","currentRoutingOrder":"1"},"purgeState":"unpurged","envelopeIdStamping":"true","is21CFRPart11":"false","signerCanSignOnMobile":"true","autoNavigation":"true","isSignatureProviderEnvelope":"false","hasFormDataChanged":"false","allowComments":"true","hasComments":"false","allowViewHistory":"true","envelopeMetadata":{"allowAdvancedCorrect":"true","enableSignWithNotary":"false","allowCorrect":"true"},"anySigner":null,"envelopeLocation":"current_site","isDynamicEnvelope":"false"}';
// Compute a hash as in production this will come from DocuSign
$hash = $this->computeHash($this->docusignConnectKey, $payload);
// Validate the hash as we're going to use it as the header
$this->assertTrue($this->validateHash($this->docusignConnectKey, $payload, $hash));
// Convert this response to an array for the test
$payload = json_decode($payload, true);
// Post as JSON as Laravel only accepts POSTing arrays
$this->postJson(route('webhook-docusign'), $payload, [
'x-docusign-signature-3' => $hash
])->assertStatus(200);
$this->assertDatabaseHas('payment_attempts', [
'envelope_id' => $payload['envelopeId']
]);
Mail::assertNothingSent();
}
/**
* As we're testing we need a way to verify the signature so we're computing the hash.
*/
private function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* #param string $secret: the secret known only by our application
* #param string $payload: the payload received from the webhook
* #param string $verify: the string we want to verify in the request header
*/
private function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
I'm also using webhook.site to compare hashes:
Given this I can tell you that x-docusign-signature-3 matches the hash I generate when I run
$hash = $this->computeHash($this->docusignConnectKey, $payload);
So, my issue surely must stem from the way I'm sending the data back through?
When you compute your own HMAC on the incoming payload (to see if it matches the HMAC that was sent in the header), you must use the incoming payload as is.
In your code:
public function handleWebhook(Request $request)
{
$payload = json_decode($request->getContent(), true);
$shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);
you are sending the json decoded payload to your check method. That is not right, you should send the raw payload, as it arrived.
(Decoding, then encoding JSON doesn't necessarily give you the same byte sequence as the original.)
The JSON decode method should only be applied to the payload after you've confirmed that the payload came from DocuSign.
Plus, doing the JSON decode before you've authenticated the sender is a security issue. A bad guy could be trying to send you some bad input. The rule is trust nothing until you've verified the sender (via the HMAC in this case).
Bonus comment
I recommend that you also configure DocuSign Connect webhook's Basic Authentication feature. Basic Authentication is often checked at the web server level. HMAC, since it must be computed, is usually check at the app level. Using both provides solid defense against bad guys.

Amazon ElasticSearch service Signature mismatch for PUT Request - Amazon SDK php V2

I am using Amazon ElasticSearch Service and when i tried to create SignatureV4 Request it is working fine for search operations (GET Requests). But when i tried to do some operations like create indices (Using PUT request), it will trough the Signature mismatch error.
I am using Amazon SDK version 2 SignatureV4 library for signing the requests. Also created a custom Elasticsearch handler to add tokens to the request.
Does anybody have such issue with SignatureV4 library in Amazon SDK php V2.
{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'PUT\n/test_index_2\n\nhost:search-test-gps2gj4zx654muo6a5m3vxm3cy.eu-west-1.es.amazonaws.com\nx-amz-date:XXXXXXXXXXXX\n\nhost;x-amz-date\n271d5ef919251148dc0b5b3f3968c3debc911a41b60ef4e92c55b98057d6cdd4'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\XXXXXXXXXXXX\n20170511/eu-west-1/es/aws4_request\n0bd34812e0727fba7c54068b0ae1114db235cfc2f97059b88be43e8b264e1d57'\n"}
This tweak only necessary for the users who are still using Amazon SDK PHP version 2. In version 3, it supported by default.
For signed request i updated the current elsticsearch client handler by adding a middle ware for signing the request.
$elasticConfig = Configure::read('ElasticSearch');
$middleware = new AwsSignatureMiddleware();
$defaultHandler = \Elasticsearch\ClientBuilder::defaultHandler();
$awsHandler = $middleware($defaultHandler);
$clientBuilder = \Elasticsearch\ClientBuilder::create();
$clientBuilder->setHandler($awsHandler)
->setHosts([$elasticConfig['host'].':'.$elasticConfig['port']]);
$client = $clientBuilder->build();
I used the following library for this purpose
use Aws\Common\Credentials\CredentialsInterface;
use Aws\Common\Signature\SignatureInterface;
use Guzzle\Http\Message\Request;
class AwsSignatureMiddleware
{
/**
* #var \Aws\Credentials\CredentialsInterface
*/
protected $credentials;
/**
* #var \Aws\Signature\SignatureInterface
*/
protected $signature;
/**
* #param CredentialsInterface $credentials
* #param SignatureInterface $signature
*/
public function __construct()
{
$amazonConf = Configure::read('AmazonSDK');
$this->credentials = new \Aws\Common\Credentials\Credentials($amazonConf['key'], $amazonConf['secret']);
$this->signature = new \Aws\Common\Signature\SignatureV4('es', 'eu-west-1');
}
/**
* #param $handler
* #return callable
*/
public function __invoke($handler)
{
return function ($request) use ($handler) {
$headers = $request['headers'];
if ($headers['host']) {
if (is_array($headers['host'])) {
$headers['host'] = array_map([$this, 'removePort'], $headers['host']);
} else {
$headers['host'] = $this->removePort($headers['host']);
}
}
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
$psrRequest = new Request($request['http_method'], $request['uri'], $headers);
$this->signature->signRequest($psrRequest, $this->credentials);
$headerObj = $psrRequest->getHeaders();
$allHeaders = $headerObj->getAll();
$signedHeaders = array();
foreach ($allHeaders as $header => $allHeader) {
$signedHeaders[$header] = $allHeader->toArray();
}
$request['headers'] = array_merge($signedHeaders, $request['headers']);
return $handler($request);
};
}
protected function removePort($host)
{
return parse_url($host)['host'];
}
}
The exact line i tweaked for this purpose is
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
For PUT and POST request the payload hash was wrong because i was not considering the request body while generating payload.
Hope this code is beneficial for anyone who is using Amazon SDK PHP version 2 and using the IAM based authentication for Elasticsearch Hosted service in Amazon cloud.

PHP Soap Server without Soap Client

There are several weeks that I'm trying to create a soap server in php that at first serves a wsdl with authentication header on it and at second it accepts only authenticated users in every request. But I' ve only made it fully working only without authentication. Every search I 've made and every solution I 've found contains a SoapClient,Zend_Soap_Client,nu_soap_client (you name it) and either some kind of wrapper class around my class or only addition of username & password on the client.
But at my solution only the server is in php and client are various programs written in java etc, not in php. Here is my code for (I use zend here but the idea is the same on plain php) the wsdl generation and the server part:
use Zend\Soap\AutoDiscover as Zend_Soap_AutoDiscover;
use Zend\Soap\Server as Zend_Soap_Server;
if (isset($_GET['wsdl'])) {
$autodiscover = new Zend\Soap\AutoDiscover();
$autodiscover->setClass('MyClass');
$autodiscover->setUri('http://Myclass/path/');
$autodiscover->handle();
exit;
}
$server = new Zend_Soap_Server(null, array(
'uri' => 'http://Myclass/path/',
));
$server->setClass('Myclass');
$server->handle();
I also used piotrooo's wsdl generator and plain php soap library like this:
use WSDL\WSDLCreator;
// use WSDL\XML\Styles\DocumentLiteralWrapped;
if (isset($_GET['wsdl'])) {
$wsdl = new WSDL\WSDLCreator('Myclass', 'http://Myclass/path/');
$wsdl->setNamespace("http://Myclass/path/");
$wsdl->renderWSDL();
exit;
}
$server = new SoapServer(null, array(
'uri' => 'http://Myclass/path/',
// 'style' => SOAP_DOCUMENT,
// 'use' => SOAP_LITERAL,
));
$server->setClass('Myclass');
$server->handle();
And my class:
class Myclass
{
public function __construct()
{
/*some db stuff with doctrine*/
}
/**
* #param string $id
* #return object
*/
public function Id($id)
{
/*I'm using doctrine to fetch data from db and then return an object/or array*/
}
}
At last this is the auto generated wsdl:
<definitions name="Myclass" targetNamespace="http://Myclass/path/"><types><xsd:schema targetNamespace="http://Myclass/path/"/></types><portType name="MyclassPort"><operation name="Id"><documentation>Id</documentation><input message="tns:IdIn"/><output message="tns:IdOut"/></operation></portType><binding name="MyclassBinding" type="tns:MyclassPort"><soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/><operation name="Id"><soap:operation soapAction="http://Myclass/path/#Id"/><input><soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://Myclass/path/"/></input><output><soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://Myclass/path/"/></output></operation></binding><service name="MyclassService"><port name="MyclassPort" binding="tns:MyclassBinding"><soap:address location="http://Myclass/path/"/></port></service><message name="IdIn"><part name="id" type="xsd:string"/></message><message name="IdOut"><part name="return" type="xsd:struct"/></message></definitions>
Annotations vary on each generator.
I also tried nusoap but I was disappointed because of it's pure class method discovery. I must add that I'm testing usage of service with soapui (that's why I don't want php's SoapClient or equivalent examples).
Last I also must say that I tried solutions of adding authentication method inside my class and this worked BUT this didn't prevent unauthenticated user from accessing ws.
A little extra information. As of my research every answer I was found was about SOAPClient for example the $client->__addheader() function etc. Please tell me if I' m wrong or if this can't be done with PHP because I ll have to find someone else to do this for me with another programming language like Java etc.
Thanks in advance
Dimitris
You could have your clients send the authentication information in basically 3 places: SOAP Body, SOAP Headers, HTTP Headers. Of the three I think the proper one is SOAP Headers so I'll go with that for this example. (Note I'm using some custom auth header format, but I suggest you research about the WS-Security protocol.)
class Myclass
{
public function __construct()
{
/*some db stuff with doctrine*/
}
public function Auth($auth)
{
if (! $this->validateUser($auth->Username, $auth->Password)) {
throw new SoapFault('Client.Authentication', 'Invalid username or password');
}
}
/**
* #param string $id
* #return object
*/
public function Id($id)
{
/*...*/
}
private function validateUser($user, $password)
{
/*...*/
}
}
Now for the client: I think you in your server role don't have to worry about which particular clients will be consuming your service, simply because you can't consider them all. That's why there are standards, to abstract from particular implementations. So, as long as you follow the standard, you can trust any client will comply. And luckily, SOAP headers are part of the standard.
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Header>
<Auth>
<Username>foo</Username>
<Password>bar</Password>
</Auth>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<Id>4</Id>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
That is the XML you are expecting to receive, regardless of the client.
Finally, the WSDL. From what I've read, neither Zend Autodiscover nor piotrooo's library support headers definition. Apparently Yii's CWebService does (with proper method annotations).
In the worst case scenario, you can write the WSDL yourself, or adapt it from the one generated with your library of choice.
Links:
http://www.yiiframework.com/doc/api/1.1/CWebService
http://www.ibm.com/developerworks/library/ws-tip-headers/
UPDATE:
class Auth
{
/**
* #soap
* #var string
*/
public $Username;
/**
* #soap
* #var string
*/
public $Password;
}
class MyClass
{
private $authenticated = false;
public function Auth($auth)
{
if ($this->validateUser($auth->Username, $auth->Password)) {
$this->authenticated = true;
}
}
/**
* #soap
* #header Auth $auth
* #param string $id
* #return object
*/
public function Id($id)
{
if (! $this->authenticated) {
throw new SoapFault('Client.Authentication', 'Invalid username or password');
}
//return $id;
}
private function validateUser($user, $password)
{
return $user == 'foo';
}
}
require __DIR__ . '/vendor/yiisoft/yii/framework/yii.php';
$URL = 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'];
if (isset($_GET['wsdl'])) {
$gen = new CWsdlGenerator();
$wsdl = $gen->generateWsdl(MyClass::class, $URL);
header("Content-type: text/xml; charset=utf-8");
echo $wsdl;
}
else {
$server = new SoapServer($URL . '?wsdl');
$server->setClass(MyClass::class);
echo $server->handle();
}
This is indeed working as expected with the sample XML I provided before.
(You just need to change <Id>4</Id> for <Id><id>4</id></Id>).

Zend Soap Autodiscovery Allow Array - Web Service

I am new to WebServices in General, and so far I have developed a Web Service SoapServer using Zend/Soap. I have been able to use it just fine, the problem is, that i want the client to be able to send the data as and array .
So far this is what i have done:
soap_server.php
<?php
/*
* url_helper used to check the client Access Ip adrress and Port, in case is from dev or prod enviorement. etc.
* returns the portion of the URL dynamicly in case its accesded from a public or local location.
*
*/
function url_helper(){
$s = empty($_SERVER["HTTPS"]) ? '' : ($_SERVER["HTTPS"] == "on") ? "s" : "";
$sp = strtolower($_SERVER["SERVER_PROTOCOL"]);
$protocol = substr($sp, 0, strpos($sp, "/")) . $s;
$port = ($_SERVER["SERVER_PORT"] == "80") ? "" : (":".$_SERVER["SERVER_PORT"]);
return $protocol . "://" . $_SERVER['SERVER_NAME'] . $port;
}
if(($_SERVER['PHP_AUTH_USER'] == "test") AND ($_SERVER['PHP_AUTH_PW'] == "secret")){
//autoload from composer, loads all the required files for ZendSoap
include("vendor/autoload.php");
$serviceURL = url_helper().'/soap_server.php';
//The class for the WebService
class soap_server{
/**
*
* #param string $str1
* #param string $str2
* #param string $str3
* #return stdClass
*/
public function TEST($str1,$str2,$str3) {
// do some work here he
$response = new stdClass();
$response->message = "vars = ($str1,$str2,$str3)";
$response->success = true;
return $response;
}
}
// Generate WSDL relevant to code
if (isset($_GET['wsdl'])){
$autodiscover = new Zend\Soap\AutoDiscover();
$autodiscover->setClass('soap_server')
->setUri($serviceURL)
->setServiceName('soap_server');
$autodiscover->generate();
$autodiscover->handle();
//Soap Server
} else {
$server = new Zend\Soap\Server(null,array('uri' => $serviceURL.'?wsdl'));
$server->setClass('soap_server');
$server->handle();
}
}
else
{
//Send headers to cause a browser to request
//username and password from user
header("WWW-Authenticate: " .
"Basic realm=\"Protected Area\"");
header("HTTP/1.0 401 Unauthorized");
//Show failure text, which browsers usually
//show only after several failed attempts
print("This page is protected by HTTP " .
"Authentication.<br>\nUse <b>User</b> " .
"for the username, and <b>PW</b> " .
"for the password.<br>\n");
}
And the test client works fine when i send at as String, as intended and defined in the soapServer:
test_client.php
include("vendor/autoload.php");
$client = new Zend\Soap\Client("http://127.0.0.1/soap_server.php?wsdl",array('login'=>'test','password'=>'secret'));
$result1 = $client->TEST('Data1','OtherString','test');
print_r($result1);
All i need now if find a way so that the client can send me data in and Array like:
$data = array('str1'=>'Data1','str2'=>'OtherString','str3'=>'test');
But i don't know how to set this up with Zend Framework Autodiscovery and pair it with a working client. Have tried using type array instead of String, but with no success.
Thanks a lot for any help.
Sincerely,
Daniel
EDIT
So I have done further testing, and i do get to set up and Array or and stdClass in the docblock and it works in the following way:
The sample Zend Framework Soap Server:
$serviceURL = url_helper().'/test_ws.php';
//The class for the WebService
class test_ws{
/**
*
* #param string $str1
* #param array $myArray
* #param stdClass $myObject test
* #return stdClass
*/
public function TEST2($str1,$myArray,$myObject) {
// do some work here
$response = new stdClass();
$response->string = $str1;
$response->array = var_export($myArray,TRUE);
$response->stdClass = var_export($myObject,TRUE);
$response->object1 = $myObject->obj1;
$response->success = true;
return $response;
}
}
// Generate WSDL relevant to code
if (isset($_GET['wsdl'])){
$autodiscover = new Zend\Soap\AutoDiscover();
$autodiscover->setClass('test_ws')
->setUri($serviceURL)
->setServiceName('test_ws');
$autodiscover->generate();
$autodiscover->handle();
//Soap Server
} else {
$server = new Zend\Soap\Server(null,array('uri' => $serviceURL.'?wsdl'));
$server->setClass('test_ws');
$server->handle();
}
}
The Soap Call:
<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:test="http://10.1.11.122/ws_kioskos/test_ws.php">
<soapenv:Header/>
<soapenv:Body>
<test:TEST2 soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<str1 xsi:type="xsd:string">String</str1>
<myArray xsi:type="soapenc:Array" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<!--You may enter ANY elements at this point-->
<elem1 xsi:type="xsd:string">pos1</elem1>
<elem2 xsi:type="xsd:string">pos2</elem2>
</myArray>
<myObject xsi:type="test:stdClass">
<obj1 xsi:type="xsd:string">HELO</obj1>
<obj2 xsi:type="xsd:string">WORLD</obj2>
</myObject>
</test:TEST2>
</soapenv:Body>
</soapenv:Envelope>
And the Soap Response:
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://10.1.11.122/ws_kioskos/test_ws.php?wsdl" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:TEST2Response>
<return xsi:type="SOAP-ENC:Struct">
<string xsi:type="xsd:string">String</string>
<array xsi:type="xsd:string">array (0 => 'pos1', 1 => 'pos2',)</array>
<stdClass xsi:type="xsd:string">stdClass::__set_state(array('obj1' => 'HELO', 'obj2' => 'WORLD',))</stdClass>
<object1 xsi:type="xsd:string">HELO</object1>
<success xsi:type="xsd:boolean">true</success>
</return>
</ns1:TEST2Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
So with The stdClass I´m close to what i want to implement, as you can see with the:
<object1 xsi:type="xsd:string">HELO</object1> the only think missing is If there is a way to tell the Autodiscover Method what are the elements that are inside the stdClass, so that the client know how to interact with the WSDL, or is there another approach that would let me do such think.
I have read some about using Complex Data types, and using a ClassMap to define the WebService, but i could not make anything work with it, as i could not find good documentation for such implementation.
Once again,
Thanks a lot for any help.
Daniel
Working example
After some research, here is the solution that would be useful to anyone who builds SOAP server with zend-soap in Laravel.
Acme\Controllers\SoapController class:
...
public function wsdl()
{
$wsdl = new AutoDiscover(new ArrayOfTypeComplex());
$this->populateServer($wsdl);
return response($wsdl->toXml(), 200)
->header('Content-Type', 'application/wsdl+xml');
}
private function populateServer($server)
{
$server->setClass(MyService::class);
$server->setUri('http://host.com/soap/server');
}
public function server(Request $request)
{
$server = new Zend\Soap\ServerServer();
$this->populateServer($server);
$response = $server->handle();
return response($response, 200)->header('Content-Type', 'application/soap+xml');
}
...
Acme\Services\MyService class:
namespace Acme\Services;
class MyService
{
/**
* Does something.
* #param Acme\Types\ItemType[] $items
* #return \StdClass
*/
public function doSomething(array $items)
{
// ...
}
}
Acme\Types\ItemType class:
namespace Acme\Types;
class ItemType
{
/**
* #var string
*/
public $propertyName;
}
Please note two things:
Use Zend\Soap\Wsdl\ComplexTypeStrategy\ArrayOfTypeComplex strategy; and
Provide fully-qualified class name in a docblock.
Hope that helps.

How can i create a soap header like this?

Doing some SOAP calls to a 3rd party application. They provide this soap header as an example of what the application expects. How can I create a SOAP header like this in PHP?
<SOAP-ENV:Header>
<NS1:Security xsi:type="NS2:Security" xmlns:NS1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:NS2="urn:dkWSValueObjects">
<NS2:UsernameToken xsi:type="NS2:TUsernameToken">
<Username xsi:type="xsd:string">XXXX</Username>
<Password xsi:type="xsd:string">XXX</Password>
</NS2:UsernameToken>
</NS1:Security>
</SOAP-ENV:Header>
I do what i think is a correct call and keep getting in return that no headers were sent.
Here is a sample from my code.
class SOAPStruct
{
function __construct($user, $pass)
{
$this->Username = $user;
$this->Password = $pass;
}
}
$client = new SoapClient("http://www.example.com/service");
$auth = new SOAPStruct("username", "password");
$header = new SoapHeader("http://example.com/service", "TUsernameToken", $auth);
$client->__setSoapHeaders(array($header));
$client->__soapCall("GetSubscriptionGroupTypes", array(), NULL, $header)
And this is the SOAP header i get back. (its more but i stripped info away that might be sensitive)
<SOAP-ENV:Header>
<ns2:TUsernameToken>
<Username>username</Username>
<Password>password</Password>
</ns2:TUsernameToken>
</SOAP-ENV:Header>
SOAP header handling in PHP is actually not very flexible and I'd go as far as saying that especially the use two namespaces within the header will make it impossible to inject the header simply by using a SoapHeader-construct of some type.
I think the best way to handle this one is to shape the XML request yourself by overriding SoapClient::__doRequest() in a custom class that extends SoapClient.
class My_SoapClient extends SoapClient
{
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$xmlRequest = new DOMDocument('1.0');
$xmlRequest->loadXML($request);
/*
* Do your processing using DOM
* e.g. insert security header and so on
*/
$request = $xmlRequest->saveXML();
return parent::__doRequest($request, $location, $action, $version, $one_way);
}
}
Please see SoapClient::__doRequest for further information and some examples.

Categories