Passed
Push — master ( 7177b3...7867d6 )
by Tim
14:55
created

HTTPRedirect::receive()   C

Complexity

Conditions 13
Paths 69

Size

Total Lines 90
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 46
c 1
b 0
f 0
nc 69
nop 1
dl 0
loc 90
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2\Binding;
6
7
use Exception;
8
use Nyholm\Psr7\Response;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use SimpleSAML\Assert\Assert;
12
use SimpleSAML\SAML2\Binding;
13
use SimpleSAML\SAML2\Compat\ContainerSingleton;
14
use SimpleSAML\SAML2\Constants as C;
15
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
16
use SimpleSAML\SAML2\Utils;
17
use SimpleSAML\SAML2\XML\samlp\AbstractMessage;
18
use SimpleSAML\SAML2\XML\samlp\AbstractRequest;
19
use SimpleSAML\SAML2\XML\samlp\MessageFactory;
20
use SimpleSAML\XML\DOMDocumentFactory;
21
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
22
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
23
use SimpleSAML\XMLSecurity\TestUtils\PEMCertificatesMock;
24
25
use function array_key_exists;
26
use function base64_decode;
27
use function base64_encode;
28
use function gzdeflate;
29
use function gzinflate;
30
use function sprintf;
31
use function str_contains;
32
use function strlen;
33
use function urlencode;
34
35
/**
36
 * Class which implements the HTTP-Redirect binding.
37
 *
38
 * @package simplesamlphp/saml2
39
 */
40
class HTTPRedirect extends Binding
41
{
42
    /**
43
     * Create the redirect URL for a message.
44
     *
45
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $message The message.
46
     * @return string The URL the user should be redirected to in order to send a message.
47
     */
48
    public function getRedirectURL(AbstractMessage $message): string
49
    {
50
        if ($this->destination === null) {
51
            $destination = $message->getDestination();
52
            if ($destination === null) {
53
                throw new Exception('Cannot build a redirect URL, no destination set.');
54
            }
55
        } else {
56
            $destination = $this->destination;
57
        }
58
59
        $relayState = $this->getRelayState();
60
        $msgStr = $message->toXML();
61
62
        Utils::getContainer()->debugMessage($msgStr, 'out');
63
        $msgStr = $msgStr->ownerDocument?->saveXML($msgStr);
64
65
        $msgStr = gzdeflate($msgStr);
66
        $msgStr = base64_encode($msgStr);
67
68
        /* Build the query string. */
69
70
        if ($message instanceof AbstractRequest) {
71
            $msg = 'SAMLRequest=';
72
        } else {
73
            $msg = 'SAMLResponse=';
74
        }
75
        $msg .= urlencode($msgStr);
76
77
        if ($relayState !== null) {
78
            $msg .= '&RelayState=' . urlencode($relayState);
79
        }
80
81
        $signature = $message->getSignature();
82
        if ($signature !== null) { // add the signature
83
            $msg .= '&SigAlg=' . urlencode($signature->getSignedInfo()->getSignatureMethod()->getAlgorithm());
84
            $msg .= '&Signature=' . urlencode($signature->getSignatureValue()->getContent());
85
        }
86
87
        if (str_contains($destination, '?')) {
88
            $destination .= '&' . $msg;
89
        } else {
90
            $destination .= '?' . $msg;
91
        }
92
93
        return $destination;
94
    }
95
96
97
    /**
98
     * Send a SAML 2 message using the HTTP-Redirect binding.
99
     *
100
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $message
101
     * @return \Psr\Http\Message\ResponseInterface
102
     */
103
    public function send(AbstractMessage $message): ResponseInterface
104
    {
105
        $destination = $this->getRedirectURL($message);
106
        Utils::getContainer()->getLogger()->debug(
107
            'Redirect to ' . strlen($destination) . ' byte URL: ' . $destination,
108
        );
109
        return new Response(303, ['Location' => $destination]);
110
    }
111
112
113
    /**
114
     * Receive a SAML 2 message sent using the HTTP-Redirect binding.
115
     *
116
     * Throws an exception if it is unable receive the message.
117
     *
118
     * @param \Psr\Http\Message\ServerRequestInterface $request
119
     * @return \SimpleSAML\SAML2\XML\samlp\AbstractMessage The received message.
120
     * @throws \Exception
121
     *
122
     * NPath is currently too high but solving that just moves code around.
123
     */
124
    public function receive(ServerRequestInterface $request): AbstractMessage
125
    {
126
        $query = $this->parseQuery();
127
        $signedQuery = $query['SignedQuery'];
128
129
        /**
130
         * Get the SAMLRequest/SAMLResponse from the exact same signed data that will be verified later in
131
         * validateSignature into $res using the actual SignedQuery
132
         */
133
        $res = [];
134
        foreach (explode('&', $signedQuery) as $e) {
135
            $tmp = explode('=', $e, 2);
136
            $name = $tmp[0];
137
            if (count($tmp) === 2) {
138
                $value = $tmp[1];
139
            } else {
140
                /* No value for this parameter. */
141
                $value = '';
142
            }
143
            $name = urldecode($name);
144
            $res[$name] = urldecode($value);
145
        }
146
147
        /**
148
         * Put the SAMLRequest/SAMLResponse from the actual query string into $message,
149
         * and assert that the result from parseQuery() in $query and the parsing of the SignedQuery in $res agree
150
         */
151
        if (array_key_exists('SAMLRequest', $res)) {
152
            Assert::same($res['SAMLRequest'], $query['SAMLRequest'], 'Parse failure.');
153
            $message = $res['SAMLRequest'];
154
        } elseif (array_key_exists('SAMLResponse', $res)) {
155
            Assert::same($res['SAMLResponse'], $query['SAMLResponse'], 'Parse failure.');
156
            $message = $res['SAMLResponse'];
157
        } else {
158
            throw new Exception('Missing SAMLRequest or SAMLResponse parameter.');
159
        }
160
161
        if (isset($query['SAMLEncoding']) && $query['SAMLEncoding'] !== C::BINDING_HTTP_REDIRECT_DEFLATE) {
162
            throw new Exception(sprintf('Unknown SAMLEncoding: %s', $query['SAMLEncoding']));
163
        }
164
165
        $message = base64_decode($message, true);
166
        if ($message === false) {
167
            throw new Exception('Error while base64 decoding SAML message.');
168
        }
169
170
        $message = gzinflate($message);
171
        if ($message === false) {
172
            throw new Exception('Error while inflating SAML message.');
173
        }
174
175
        $document = DOMDocumentFactory::fromString($message);
176
        Utils::getContainer()->debugMessage($document->documentElement, 'in');
177
        $message = MessageFactory::fromXML($document->documentElement);
178
179
        if (array_key_exists('RelayState', $query)) {
180
            $this->setRelayState($query['RelayState']);
181
        }
182
183
        if (!array_key_exists('Signature', $query)) {
184
            return $message;
185
        }
186
187
        /**
188
         * 3.4.5.2 - SAML Bindings
189
         *
190
         * If the message is signed, the Destination XML attribute in the root SAML element of the protocol
191
         * message MUST contain the URL to which the sender has instructed the user agent to deliver the
192
         * message.
193
         */
194
        Assert::notNull($message->getDestination(), ProtocolViolationException::class);
195
        // Validation of the Destination must be done upstream
196
197
        if (!array_key_exists('SigAlg', $query)) {
198
            throw new Exception('Missing signature algorithm.');
199
        }
200
201
        $container = ContainerSingleton::getInstance();
202
        $blacklist = $container->getBlacklistedEncryptionAlgorithms();
203
        $verifier = (new SignatureAlgorithmFactory($blacklist))->getAlgorithm(
0 ignored issues
show
Bug introduced by
It seems like $blacklist can also be of type null; however, parameter $blacklist of SimpleSAML\XMLSecurity\A...mFactory::__construct() does only seem to accept array, 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

203
        $verifier = (new SignatureAlgorithmFactory(/** @scrutinizer ignore-type */ $blacklist))->getAlgorithm(
Loading history...
204
            $query['SigAlg'],
205
            // TODO:  Need to use the key from the metadata
206
            PEMCertificatesMock::getPublicKey(PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY),
207
        );
208
209
        if ($verifier->verify($signedQuery, base64_decode($query['Signature'])) === false) {
210
            throw new SignatureVerificationFailedException('Failed to verify signature.');
211
        }
212
213
        return $message;
214
    }
215
216
217
    /**
218
     * Helper function to parse query data.
219
     *
220
     * This function returns the query string split into key=>value pairs.
221
     * It also adds a new parameter, SignedQuery, which contains the data that is
222
     * signed.
223
     *
224
     * @return array The query data that is signed.
225
     * @throws \Exception
226
     */
227
    private static function parseQuery() : array
228
    {
229
        /*
230
         * Parse the query string. We need to do this ourself, so that we get access
231
         * to the raw (urlencoded) values. This is required because different software
232
         * can urlencode to different values.
233
         */
234
        $data = [];
235
        $relayState = '';
236
        $sigAlg = '';
237
        $sigQuery = '';
238
239
        foreach (explode('&', $_SERVER['QUERY_STRING']) as $e) {
240
            $tmp = explode('=', $e, 2);
241
            $name = $tmp[0];
242
            if (count($tmp) === 2) {
243
                $value = $tmp[1];
244
            } else {
245
                /* No value for this parameter. */
246
                $value = '';
247
            }
248
249
            $name = urldecode($name);
250
            // Prevent keys from being set more than once
251
            if (array_key_exists($name, $data)) {
252
                throw new Exception('Duplicate parameter.');
253
            }
254
            $data[$name] = urldecode($value);
255
256
            switch ($name) {
257
                case 'SAMLRequest':
258
                case 'SAMLResponse':
259
                    $sigQuery = $name . '=' . $value;
260
                    break;
261
                case 'RelayState':
262
                    $relayState = '&RelayState=' . $value;
263
                    break;
264
                case 'SigAlg':
265
                    $sigAlg = '&SigAlg=' . $value;
266
                    break;
267
            }
268
        }
269
270
        if (array_key_exists('SAMLRequest', $data) && array_key_exists('SAMLResponse', $data)) {
271
                throw new Exception('Both SAMLRequest and SAMLResponse provided.');
272
        }
273
274
        $data['SignedQuery'] = $sigQuery . $relayState . $sigAlg;
275
276
        return $data;
277
    }
278
}
279