Passed
Push — master ( ec27ff...f3e803 )
by Tim
02:29
created

HTTPRedirect::getRedirectURL()   B

Complexity

Conditions 7
Paths 33

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 28
nc 33
nop 1
dl 0
loc 46
rs 8.5386
c 0
b 0
f 0
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\Utils;
16
use SimpleSAML\SAML2\XML\samlp\AbstractMessage;
17
use SimpleSAML\SAML2\XML\samlp\AbstractRequest;
18
use SimpleSAML\SAML2\XML\samlp\MessageFactory;
19
use SimpleSAML\XML\DOMDocumentFactory;
20
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
21
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
22
use SimpleSAML\XMLSecurity\TestUtils\PEMCertificatesMock;
23
24
use function array_key_exists;
25
use function base64_decode;
26
use function base64_encode;
27
use function gzdeflate;
28
use function gzinflate;
29
use function sprintf;
30
use function str_contains;
31
use function strlen;
32
use function urlencode;
33
34
/**
35
 * Class which implements the HTTP-Redirect binding.
36
 *
37
 * @package simplesamlphp/saml2
38
 */
39
class HTTPRedirect extends Binding
40
{
41
    /**
42
     * Create the redirect URL for a message.
43
     *
44
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $message The message.
45
     * @return string The URL the user should be redirected to in order to send a message.
46
     */
47
    public function getRedirectURL(AbstractMessage $message): string
48
    {
49
        if ($this->destination === null) {
50
            $destination = $message->getDestination();
51
            if ($destination === null) {
52
                throw new Exception('Cannot build a redirect URL, no destination set.');
53
            }
54
        } else {
55
            $destination = $this->destination;
56
        }
57
58
        $relayState = $this->getRelayState();
59
        $msgStr = $message->toXML();
60
61
        Utils::getContainer()->debugMessage($msgStr, 'out');
62
        $msgStr = $msgStr->ownerDocument?->saveXML($msgStr);
63
64
        $msgStr = gzdeflate($msgStr);
65
        $msgStr = base64_encode($msgStr);
66
67
        /* Build the query string. */
68
69
        if ($message instanceof AbstractRequest) {
70
            $msg = 'SAMLRequest=';
71
        } else {
72
            $msg = 'SAMLResponse=';
73
        }
74
        $msg .= urlencode($msgStr);
75
76
        if ($relayState !== null) {
77
            $msg .= '&RelayState=' . urlencode($relayState);
78
        }
79
80
        $signature = $message->getSignature();
81
        if ($signature !== null) { // add the signature
82
            $msg .= '&SigAlg=' . urlencode($signature->getSignedInfo()->getSignatureMethod()->getAlgorithm());
83
            $msg .= '&Signature=' . urlencode($signature->getSignatureValue()->getContent());
84
        }
85
86
        if (str_contains($destination, '?')) {
87
            $destination .= '&' . $msg;
88
        } else {
89
            $destination .= '?' . $msg;
90
        }
91
92
        return $destination;
93
    }
94
95
96
    /**
97
     * Send a SAML 2 message using the HTTP-Redirect binding.
98
     *
99
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $message
100
     * @return \Psr\Http\Message\ResponseInterface
101
     */
102
    public function send(AbstractMessage $message): ResponseInterface
103
    {
104
        $destination = $this->getRedirectURL($message);
105
        Utils::getContainer()->getLogger()->debug(
106
            'Redirect to ' . strlen($destination) . ' byte URL: ' . $destination,
107
        );
108
        return new Response(303, ['Location' => $destination]);
109
    }
110
111
112
    /**
113
     * Receive a SAML 2 message sent using the HTTP-Redirect binding.
114
     *
115
     * Throws an exception if it is unable receive the message.
116
     *
117
     * @param \Psr\Http\Message\ServerRequestInterface $request
118
     * @return \SimpleSAML\SAML2\XML\samlp\AbstractMessage The received message.
119
     * @throws \Exception
120
     *
121
     * NPath is currently too high but solving that just moves code around.
122
     */
123
    public function receive(ServerRequestInterface $request): AbstractMessage
124
    {
125
        $query = $request->getQueryParams();
126
127
        if (array_key_exists('SAMLRequest', $query)) {
128
            $message = $query['SAMLRequest'];
129
            $signedQuery = 'SAMLRequest=' . urlencode($query['SAMLRequest']);
130
        } elseif (array_key_exists('SAMLResponse', $query)) {
131
            $message = $query['SAMLResponse'];
132
            $signedQuery = 'SAMLResponse=' . urlencode($query['SAMLResponse']);
133
        } else {
134
            throw new Exception('Missing SAMLRequest or SAMLResponse parameter.');
135
        }
136
137
        if (isset($query['SAMLEncoding']) && $query['SAMLEncoding'] !== C::BINDING_HTTP_REDIRECT_DEFLATE) {
138
            throw new Exception(sprintf('Unknown SAMLEncoding: %s', $query['SAMLEncoding']));
139
        }
140
141
        $message = base64_decode($message, true);
142
        if ($message === false) {
143
            throw new Exception('Error while base64 decoding SAML message.');
144
        }
145
146
        $message = gzinflate($message);
147
        if ($message === false) {
148
            throw new Exception('Error while inflating SAML message.');
149
        }
150
151
        $document = DOMDocumentFactory::fromString($message);
152
        Utils::getContainer()->debugMessage($document->documentElement, 'in');
153
        $message = MessageFactory::fromXML($document->documentElement);
154
155
        if (array_key_exists('RelayState', $query)) {
156
            $signedQuery .= '&RelayState=' . urlencode($query['RelayState']);
157
            $this->setRelayState($query['RelayState']);
158
        }
159
160
        if (!array_key_exists('Signature', $query)) {
161
            return $message;
162
        }
163
164
        /**
165
         * 3.4.5.2 - SAML Bindings
166
         *
167
         * If the message is signed, the Destination XML attribute in the root SAML element of the protocol
168
         * message MUST contain the URL to which the sender has instructed the user agent to deliver the
169
         * message.
170
         */
171
        Assert::notNull($message->getDestination()); // Validation of the value must be done upstream
172
173
        if (!array_key_exists('SigAlg', $query)) {
174
            throw new Exception('Missing signature algorithm.');
175
        } else {
176
            $signedQuery .= '&SigAlg=' . urlencode($query['SigAlg']);
177
        }
178
179
        $container = ContainerSingleton::getInstance();
180
        $blacklist = $container->getBlacklistedEncryptionAlgorithms();
181
        $verifier = (new SignatureAlgorithmFactory($blacklist))->getAlgorithm(
182
            $query['SigAlg'],
183
            // TODO:  Need to use the key from the metadata
184
            PEMCertificatesMock::getPublicKey(PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY),
185
        );
186
187
        if ($verifier->verify($signedQuery, base64_decode($query['Signature'])) === false) {
188
            throw new SignatureVerificationFailedException('Failed to verify signature.');
189
        }
190
191
        return $message;
192
    }
193
}
194