Passed
Push — master ( 0e8d42...4929f2 )
by Tim
02:37
created

SOAPClient   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 107
dl 0
loc 230
rs 10
c 0
b 0
f 0
wmc 30

4 Methods

Rating   Name   Duplication   Size   Complexity  
F send() 0 128 19
A getSOAPFault() 0 12 2
A validateSSL() 0 20 4
A addSSLValidator() 0 29 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2;
6
7
use DOMDocument;
8
use Exception;
9
use SimpleSAML\Configuration;
1 ignored issue
show
Bug introduced by
The type SimpleSAML\Configuration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use SimpleSAML\SAML2\Compat\ContainerSingleton;
11
use SimpleSAML\SAML2\Exception\InvalidArgumentException;
12
use SimpleSAML\SAML2\Exception\RuntimeException;
13
use SimpleSAML\SAML2\Utils\XPath;
14
use SimpleSAML\SAML2\XML\samlp\AbstractMessage;
15
use SimpleSAML\SAML2\XML\samlp\MessageFactory;
16
use SimpleSAML\SOAP11\XML\Body;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SOAP11\XML\Body was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use SimpleSAML\SOAP11\XML\Envelope;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SOAP11\XML\Envelope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use SimpleSAML\SOAP11\XML\Fault;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SOAP11\XML\Fault was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use SimpleSAML\Utils\Config;
1 ignored issue
show
Bug introduced by
The type SimpleSAML\Utils\Config was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use SimpleSAML\Utils\Crypto;
1 ignored issue
show
Bug introduced by
The type SimpleSAML\Utils\Crypto was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use SimpleSAML\XML\Exception\UnparseableXMLException;
22
use SimpleSAML\XMLSecurity\XMLSecurityKey;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XMLSecurityKey was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use SoapClient as BuiltinSoapClient;
24
25
use function chunk_split;
26
use function file_exists;
27
use function openssl_pkey_get_details;
28
use function openssl_pkey_get_public;
29
use function sha1;
30
use function stream_context_create;
31
use function stream_context_get_options;
32
33
/**
34
 * Implementation of the SAML 2.0 SOAP binding.
35
 *
36
 * @package simplesamlphp/saml2
37
 */
38
class SOAPClient
39
{
40
    /**
41
     * This function sends the SOAP message to the service location and returns SOAP response
42
     *
43
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $msg The request that should be sent.
44
     * @param \SimpleSAML\Configuration $srcMetadata The metadata of the issuer of the message.
45
     * @param \SimpleSAML\Configuration $dstMetadata The metadata of the destination of the message.
46
     * @throws \Exception
47
     * @return \SimpleSAML\SAML2\XML\samlp\AbstractMessage The response we received.
48
     *
49
     * @psalm-suppress UndefinedClass
50
     */
51
    public function send(AbstractMessage $msg, Configuration $srcMetadata, Configuration $dstMetadata = null): AbstractMessage
52
    {
53
        $issuer = $msg->getIssuer();
54
55
        $ctxOpts = [
56
            'ssl' => [
57
                'capture_peer_cert' => true,
58
                'allow_self_signed' => true
59
            ],
60
        ];
61
62
        $container = ContainerSingleton::getInstance();
63
64
        // Determine if we are going to do a MutualSSL connection between the IdP and SP  - Shoaib
65
        if ($srcMetadata->hasValue('saml.SOAPClient.certificate')) {
66
            $cert = $srcMetadata->getValue('saml.SOAPClient.certificate');
67
            if ($cert !== false) {
68
                $ctxOpts['ssl']['local_cert'] = Config::getCertPath(
69
                    $srcMetadata->getString('saml.SOAPClient.certificate')
70
                );
71
                if ($srcMetadata->hasValue('saml.SOAPClient.privatekey_pass')) {
72
                    $ctxOpts['ssl']['passphrase'] = $srcMetadata->getString('saml.SOAPClient.privatekey_pass');
73
                }
74
            }
75
        } else {
76
            /* Use the SP certificate and privatekey if it is configured. */
77
            $privateKey = Crypto::loadPrivateKey($srcMetadata);
78
            $publicKey = Crypto::loadPublicKey($srcMetadata);
79
            if ($privateKey !== null && $publicKey !== null && isset($publicKey['PEM'])) {
80
                $keyCertData = $privateKey['PEM'] . $publicKey['PEM'];
81
                $file = $container->getTempDir() . '/' . sha1($keyCertData) . '.pem';
82
                if (!file_exists($file)) {
83
                    $container->writeFile($file, $keyCertData);
84
                }
85
                $ctxOpts['ssl']['local_cert'] = $file;
86
                if (isset($privateKey['password'])) {
87
                    $ctxOpts['ssl']['passphrase'] = $privateKey['password'];
88
                }
89
            }
90
        }
91
92
        // do peer certificate verification
93
        if ($dstMetadata !== null) {
94
            $peerPublicKeys = $dstMetadata->getPublicKeys('signing', true);
95
            $certData = '';
96
            foreach ($peerPublicKeys as $key) {
97
                if ($key['type'] !== 'X509Certificate') {
98
                    continue;
99
                }
100
                $certData .= "-----BEGIN CERTIFICATE-----\n" .
101
                    chunk_split($key['X509Certificate'], 64) .
102
                    "-----END CERTIFICATE-----\n";
103
            }
104
            $peerCertFile = $container->getTempDir() . '/' . sha1($certData) . '.pem';
105
            if (!file_exists($peerCertFile)) {
106
                $container->writeFile($peerCertFile, $certData);
107
            }
108
            // create ssl context
109
            $ctxOpts['ssl']['verify_peer'] = true;
110
            $ctxOpts['ssl']['verify_depth'] = 1;
111
            $ctxOpts['ssl']['cafile'] = $peerCertFile;
112
        }
113
114
        if ($srcMetadata->hasValue('saml.SOAPClient.stream_context.ssl.peer_name')) {
115
            $ctxOpts['ssl']['peer_name'] = $srcMetadata->getString('saml.SOAPClient.stream_context.ssl.peer_name');
116
        }
117
118
        $context = stream_context_create($ctxOpts);
119
120
        $options = [
121
            'uri' => $issuer->getContent(),
122
            'location' => $msg->getDestination(),
123
            'stream_context' => $context,
124
        ];
125
126
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyhost')) {
127
            $options['proxy_host'] = $srcMetadata->getValue('saml.SOAPClient.proxyhost');
128
        }
129
130
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyport')) {
131
            $options['proxy_port'] = $srcMetadata->getValue('saml.SOAPClient.proxyport');
132
        }
133
134
        $destination = $msg->getDestination();
135
        if ($destination === null) {
136
            throw new Exception('Cannot send SOAP message, no destination set.');
137
        }
138
139
        // Add soap-envelopes
140
        $env = (new Envelope(new Body([new Chunk($msg->toXML())])))->toXML();
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SAML2\Chunk was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
141
        $request = $env->ownerDocument->saveXML();
142
143
        $container->debugMessage($request, 'out');
144
145
        $action = 'http://www.oasis-open.org/committees/security';
146
        /* Perform SOAP Request over HTTP */
147
        $x = new BuiltinSoapClient(null, $options);
148
        $soapresponsexml = $x->__doRequest($request, $destination, $action, SOAP_1_1, false);
1 ignored issue
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $oneWay of SoapClient::__doRequest(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

148
        $soapresponsexml = $x->__doRequest($request, $destination, $action, SOAP_1_1, /** @scrutinizer ignore-type */ false);
Loading history...
149
        if (empty($soapresponsexml)) {
150
            throw new Exception('Empty SOAP response, check peer certificate.');
151
        }
152
153
        Utils::getContainer()->debugMessage($soapresponsexml, 'in');
154
155
        $env = Envelope::fromXML($dom = DOMDocumentFactory::fromString($soapresponsexml)->documentElement);
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SAML2\DOMDocumentFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
156
        $container->debugMessage($env->toXML()->ownerDocument->saveXML(), 'in');
157
158
        $soapfault = $this->getSOAPFault($dom);
159
        if ($soapfault !== null) {
160
            throw new Exception(
161
                sprintf(
162
                    "Actor: '%s';  Message: '%s';  Code: '%s'",
163
                    $soapfault->getFaultActor()->getContent(),
164
                    $soapfault->getFaultString()->getContent(),
165
                    $soapfault->getFaultCode()->getContent(),
166
                ),
167
            );
168
        }
169
170
        // Extract the message from the response
171
        $samlresponse = MessageFactory::fromXML($env->getBody()->getChildren()[0]->toXML());
172
173
        /* Add validator to message which uses the SSL context. */
174
        self::addSSLValidator($samlresponse, $context);
175
176
        $container->getLogger()->debug("Valid ArtifactResponse received from IdP");
177
178
        return $samlresponse;
179
    }
180
181
182
    /**
183
     * Add a signature validator based on a SSL context.
184
     *
185
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $msg The message we should add a validator to.
186
     * @param resource $context The stream context.
187
     */
188
    private static function addSSLValidator(AbstractMessage $msg, $context): void
189
    {
190
        $options = stream_context_get_options($context);
191
        if (!isset($options['ssl']['peer_certificate'])) {
192
            return;
193
        }
194
195
        $container = ContainerSingleton::getInstance();
196
        $key = openssl_pkey_get_public($options['ssl']['peer_certificate']);
197
        if ($key === false) {
198
            $container->getLogger()->warning('Unable to get public key from peer certificate.');
199
200
            return;
201
        }
202
203
        $keyInfo = openssl_pkey_get_details($key);
204
        if ($keyInfo === false) {
205
            $container->getLogger()->warning('Unable to get key details from public key.');
206
207
            return;
208
        }
209
210
        if (!isset($keyInfo['key'])) {
211
            $container->getLogger()->warning('Missing key in public key details.');
212
213
            return;
214
        }
215
216
        $msg->addValidator([SOAPClient::class, 'validateSSL'], $keyInfo['key']);
217
    }
218
219
220
    /**
221
     * Validate a SOAP message against the certificate on the SSL connection.
222
     *
223
     * @param string $data The public key that was used on the connection.
224
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key we should validate the certificate against.
225
     * @throws \Exception
226
     */
227
    public static function validateSSL(string $data, XMLSecurityKey $key): void
228
    {
229
        $container = ContainerSingleton::getInstance();
230
        /** @psalm-suppress PossiblyNullArgument */
231
        $keyInfo = openssl_pkey_get_details($key->key);
232
        if ($keyInfo === false) {
233
            throw new Exception('Unable to get key details from XMLSecurityKey.');
234
        }
235
236
        if (!isset($keyInfo['key'])) {
237
            throw new Exception('Missing key in public key details.');
238
        }
239
240
        if ($keyInfo['key'] !== $data) {
241
            $container->getLogger()->debug('Key on SSL connection did not match key we validated against.');
242
243
            return;
244
        }
245
246
        $container->getLogger()->debug('Message validated based on SSL certificate.');
247
    }
248
249
250
    /**
251
     * Extracts the SOAP Fault from SOAP message
252
     *
253
     * @param \DOMDocument $soapMessage Soap response needs to be type DOMDocument
254
     * @return \SimpleSAML\SOAP11\XML\env\Fault|null
255
     */
256
    private function getSOAPFault(DOMDocument $soapMessage): ?Fault
257
    {
258
        $xpCache = XPath::getXPath($soapMessage->firstChild);
0 ignored issues
show
Bug introduced by
It seems like $soapMessage->firstChild can also be of type null; however, parameter $node of SimpleSAML\SAML2\Utils\XPath::getXPath() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

258
        $xpCache = XPath::getXPath(/** @scrutinizer ignore-type */ $soapMessage->firstChild);
Loading history...
259
        /** @psalm-suppress PossiblyNullArgument */
260
        $soapFault = XPath::xpQuery($soapMessage->firstChild, '/soap-env:Envelope/soap-env:Body/soap-env:Fault', $xpCache);
0 ignored issues
show
Bug introduced by
It seems like $soapMessage->firstChild can also be of type null; however, parameter $node of SimpleSAML\XML\Utils\XPath::xpQuery() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

260
        $soapFault = XPath::xpQuery(/** @scrutinizer ignore-type */ $soapMessage->firstChild, '/soap-env:Envelope/soap-env:Body/soap-env:Fault', $xpCache);
Loading history...
261
262
        if (empty($soapFault)) {
263
            /* No fault. */
264
            return null;
265
        }
266
267
        return Fault::fromXML($soapFault[0]);
268
    }
269
}
270