Completed
Push — master ( 751340...d701a1 )
by Thijs
11:10 queued 06:53
created

SAML2_SOAPClient   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 6
Metric Value
wmc 32
lcom 0
cbo 6
dl 0
loc 232
rs 9.6

4 Methods

Rating   Name   Duplication   Size   Complexity  
F send() 0 122 20
B addSSLValidator() 0 32 5
A validateSSL() 0 21 4
A getSOAPFault() 0 20 3
1
<?php
2
3
/**
4
 * Implementation of the SAML 2.0 SOAP binding.
5
 *
6
 * @author Shoaib Ali
7
 * @package SimpleSAMLphp
8
 */
9
class SAML2_SOAPClient
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
10
{
11
    const START_SOAP_ENVELOPE = '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"><soap-env:Header/><soap-env:Body>';
12
    const END_SOAP_ENVELOPE = '</soap-env:Body></soap-env:Envelope>';
13
14
    /**
15
     * This function sends the SOAP message to the service location and returns SOAP response
16
     *
17
     * @param  SAML2_Message            $msg         The request that should be sent.
18
     * @param  SimpleSAML_Configuration $srcMetadata The metadata of the issuer of the message.
19
     * @param  SimpleSAML_Configuration $dstMetadata The metadata of the destination of the message.
20
     * @return SAML2_Message            The response we received.
21
     * @throws Exception
22
     */
23
    public function send(SAML2_Message $msg, SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata = NULL)
24
    {
25
        $issuer = $msg->getIssuer();
26
27
        $ctxOpts = array(
28
            'ssl' => array(
29
                'capture_peer_cert' => TRUE,
30
            ),
31
        );
32
33
        // Determine if we are going to do a MutualSSL connection between the IdP and SP  - Shoaib
34
        if ($srcMetadata->hasValue('saml.SOAPClient.certificate')) {
35
            $cert = $srcMetadata->getValue('saml.SOAPClient.certificate');
36
            if ($cert !== FALSE) {
37
                $ctxOpts['ssl']['local_cert'] = SimpleSAML_Utilities::resolveCert(
38
                    $srcMetadata->getString('saml.SOAPClient.certificate')
39
                );
40
                if ($srcMetadata->hasValue('saml.SOAPClient.privatekey_pass')) {
41
                    $ctxOpts['ssl']['passphrase'] = $srcMetadata->getString('saml.SOAPClient.privatekey_pass');
42
                }
43
            }
44
        } else {
45
            /* Use the SP certificate and privatekey if it is configured. */
46
            $privateKey = SimpleSAML_Utilities::loadPrivateKey($srcMetadata);
47
            $publicKey = SimpleSAML_Utilities::loadPublicKey($srcMetadata);
48
            if ($privateKey !== NULL && $publicKey !== NULL && isset($publicKey['PEM'])) {
49
                $keyCertData = $privateKey['PEM'] . $publicKey['PEM'];
50
                $file = SimpleSAML_Utilities::getTempDir() . '/' . sha1($keyCertData) . '.pem';
51
                if (!file_exists($file)) {
52
                    SimpleSAML_Utilities::writeFile($file, $keyCertData);
53
                }
54
                $ctxOpts['ssl']['local_cert'] = $file;
55
                if (isset($privateKey['password'])) {
56
                    $ctxOpts['ssl']['passphrase'] = $privateKey['password'];
57
                }
58
            }
59
        }
60
61
        // do peer certificate verification
62
        if ($dstMetadata !== NULL) {
63
            $peerPublicKeys = $dstMetadata->getPublicKeys('signing', TRUE);
64
            $certData = '';
65
            foreach ($peerPublicKeys as $key) {
66
                if ($key['type'] !== 'X509Certificate') {
67
                    continue;
68
                }
69
                $certData .= "-----BEGIN CERTIFICATE-----\n" .
70
                    chunk_split($key['X509Certificate'], 64) .
71
                    "-----END CERTIFICATE-----\n";
72
            }
73
            $peerCertFile = SimpleSAML_Utilities::getTempDir() . '/' . sha1($certData) . '.pem';
74
            if (!file_exists($peerCertFile)) {
75
                SimpleSAML_Utilities::writeFile($peerCertFile, $certData);
76
            }
77
            // create ssl context
78
            $ctxOpts['ssl']['verify_peer'] = TRUE;
79
            $ctxOpts['ssl']['verify_depth'] = 1;
80
            $ctxOpts['ssl']['cafile'] = $peerCertFile;
81
        }
82
83
        $context = stream_context_create($ctxOpts);
84
        if ($context === NULL) {
85
            throw new Exception('Unable to create SSL stream context');
86
        }
87
88
        $options = array(
89
            'uri' => $issuer,
90
            'location' => $msg->getDestination(),
91
            'stream_context' => $context,
92
        );
93
94
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyhost')) {
95
            $options['proxy_host'] = $srcMetadata->getValue('saml.SOAPClient.proxyhost');
96
        }
97
98
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyport')) {
99
            $options['proxy_port'] = $srcMetadata->getValue('saml.SOAPClient.proxyport');
100
        }
101
102
        $x = new SoapClient(NULL, $options);
103
104
        // Add soap-envelopes
105
        $request = $msg->toSignedXML();
106
        $request = self::START_SOAP_ENVELOPE . $request->ownerDocument->saveXML($request) . self::END_SOAP_ENVELOPE;
107
108
        SAML2_Utils::getContainer()->debugMessage($request, 'out');
109
110
        $action = 'http://www.oasis-open.org/committees/security';
111
        $version = '1.1';
112
        $destination = $msg->getDestination();
113
114
        /* Perform SOAP Request over HTTP */
115
        $soapresponsexml = $x->__doRequest($request, $destination, $action, $version);
116
        if ($soapresponsexml === NULL || $soapresponsexml === "") {
117
            throw new Exception('Empty SOAP response, check peer certificate.');
118
        }
119
120
        SAML2_Utils::getContainer()->debugMessage($soapresponsexml, 'in');
121
122
        // Convert to SAML2_Message (DOMElement)
123
        try {
124
            $dom = SAML2_DOMDocumentFactory::fromString($soapresponsexml);
125
        } catch (SAML2_Exception_RuntimeException $e) {
126
            throw new Exception('Not a SOAP response.', 0, $e);
127
        }
128
129
        $soapfault = $this->getSOAPFault($dom);
130
        if (isset($soapfault)) {
131
            throw new Exception($soapfault);
132
        }
133
        //Extract the message from the response
134
        $samlresponse = SAML2_Utils::xpQuery($dom->firstChild, '/soap-env:Envelope/soap-env:Body/*[1]');
135
        $samlresponse = SAML2_Message::fromXML($samlresponse[0]);
136
137
        /* Add validator to message which uses the SSL context. */
138
        self::addSSLValidator($samlresponse, $context);
139
140
        SAML2_Utils::getContainer()->getLogger()->debug("Valid ArtifactResponse received from IdP");
141
142
        return $samlresponse;
143
144
    }
145
146
    /**
147
     * Add a signature validator based on a SSL context.
148
     *
149
     * @param SAML2_Message $msg     The message we should add a validator to.
150
     * @param resource      $context The stream context.
151
     */
152
    private static function addSSLValidator(SAML2_Message $msg, $context)
153
    {
154
        $options = stream_context_get_options($context);
155
        if (!isset($options['ssl']['peer_certificate'])) {
156
            return;
157
        }
158
159
        //$out = '';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
160
        //openssl_x509_export($options['ssl']['peer_certificate'], $out);
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
161
162
        $key = openssl_pkey_get_public($options['ssl']['peer_certificate']);
163
        if ($key === FALSE) {
164
            SAML2_Utils::getContainer()->getLogger()->warning('Unable to get public key from peer certificate.');
165
166
            return;
167
        }
168
169
        $keyInfo = openssl_pkey_get_details($key);
170
        if ($keyInfo === FALSE) {
171
            SAML2_Utils::getContainer()->getLogger()->warning('Unable to get key details from public key.');
172
173
            return;
174
        }
175
176
        if (!isset($keyInfo['key'])) {
177
            SAML2_Utils::getContainer()->getLogger()->warning('Missing key in public key details.');
178
179
            return;
180
        }
181
182
        $msg->addValidator(array('SAML2_SOAPClient', 'validateSSL'), $keyInfo['key']);
183
    }
184
185
    /**
186
     * Validate a SOAP message against the certificate on the SSL connection.
187
     *
188
     * @param string         $data The public key that was used on the connection.
189
     * @param XMLSecurityKey $key  The key we should validate the certificate against.
190
     * @throws Exception
191
     */
192
    public static function validateSSL($data, XMLSecurityKey $key)
193
    {
194
        assert('is_string($data)');
195
196
        $keyInfo = openssl_pkey_get_details($key->key);
197
        if ($keyInfo === FALSE) {
198
            throw new Exception('Unable to get key details from XMLSecurityKey.');
199
        }
200
201
        if (!isset($keyInfo['key'])) {
202
            throw new Exception('Missing key in public key details.');
203
        }
204
205
        if ($keyInfo['key'] !== $data) {
206
            SAML2_Utils::getContainer()->getLogger()->debug('Key on SSL connection did not match key we validated against.');
207
208
            return;
209
        }
210
211
        SAML2_Utils::getContainer()->getLogger()->debug('Message validated based on SSL certificate.');
212
    }
213
214
    /*
215
     * Extracts the SOAP Fault from SOAP message
216
     * @param $soapmessage Soap response needs to be type DOMDocument
217
     * @return $soapfaultstring string|NULL
0 ignored issues
show
Documentation introduced by
The doc-type $soapfaultstring could not be parsed: Unknown type name "$soapfaultstring" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
218
     */
219
    private function getSOAPFault($soapMessage)
220
    {
221
        $soapFault = SAML2_Utils::xpQuery($soapMessage->firstChild, '/soap-env:Envelope/soap-env:Body/soap-env:Fault');
222
223
        if (empty($soapFault)) {
224
            /* No fault. */
225
226
            return NULL;
227
        }
228
        $soapFaultElement = $soapFault[0];
229
        // There is a fault element but we haven't found out what the fault string is
230
        $soapFaultString = "Unknown fault string found";
231
        // find out the fault string
232
        $faultStringElement =   SAML2_Utils::xpQuery($soapFaultElement, './soap-env:faultstring') ;
233
        if (!empty($faultStringElement)) {
234
            return $faultStringElement[0]->textContent;
235
        }
236
237
        return $soapFaultString;
238
    }
239
240
}
241