1<?php
2
3/**
4 * SAML 2 Logout Request
5 *
6 */
7class OneLogin_Saml2_LogoutRequest
8{
9    /**
10    * Contains the ID of the Logout Request
11    * @var string
12    */
13    public $id;
14
15    /**
16     * Object that represents the setting info
17     * @var OneLogin_Saml2_Settings
18     */
19    protected $_settings;
20
21    /**
22     * SAML Logout Request
23     * @var string
24     */
25    protected $_logoutRequest;
26
27    /**
28    * After execute a validation process, this var contains the cause
29    * @var string
30    */
31    private $_error;
32
33    /**
34     * Constructs the Logout Request object.
35     *
36     * @param OneLogin_Saml2_Settings $settings Settings
37     * @param string|null $request A UUEncoded Logout Request.
38     * @param string|null $nameId The NameID that will be set in the LogoutRequest.
39     * @param string|null $sessionIndex The SessionIndex (taken from the SAML Response in the SSO process).
40     * @param string|null $nameIdFormat The NameID Format will be set in the LogoutRequest.
41     * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest.
42     * @param string|null             $nameIdSPNameQualifier The NameID SP NameQualifier will be set in the LogoutRequest.
43     *
44     * @throws OneLogin_Saml2_Error
45     */
46    public function __construct(OneLogin_Saml2_Settings $settings, $request = null, $nameId = null, $sessionIndex = null, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null)
47    {
48        $this->_settings = $settings;
49
50        $baseURL = $this->_settings->getBaseURL();
51        if (!empty($baseURL)) {
52            OneLogin_Saml2_Utils::setBaseURL($baseURL);
53        }
54
55        if (!isset($request) || empty($request)) {
56            $spData = $this->_settings->getSPData();
57            $idpData = $this->_settings->getIdPData();
58            $security = $this->_settings->getSecurityData();
59
60            $id = OneLogin_Saml2_Utils::generateUniqueID();
61            $this->id = $id;
62
63            $issueInstant = OneLogin_Saml2_Utils::parseTime2SAML(time());
64
65            $cert = null;
66            if (isset($security['nameIdEncrypted']) && $security['nameIdEncrypted']) {
67                $existsMultiX509Enc = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['encryption']) && !empty($idpData['x509certMulti']['encryption']);
68
69                if ($existsMultiX509Enc) {
70                    $cert = $idpData['x509certMulti']['encryption'][0];
71                } else {
72                    $cert = $idpData['x509cert'];
73                }
74            }
75
76            if (!empty($nameId)) {
77                if (empty($nameIdFormat) &&
78                  $spData['NameIDFormat'] != OneLogin_Saml2_Constants::NAMEID_UNSPECIFIED) {
79                    $nameIdFormat = $spData['NameIDFormat'];
80                }
81            } else {
82                $nameId = $idpData['entityId'];
83                $nameIdFormat = OneLogin_Saml2_Constants::NAMEID_ENTITY;
84            }
85
86            /* From saml-core-2.0-os 8.3.6, when the entity Format is used:
87               "The NameQualifier, SPNameQualifier, and SPProvidedID attributes MUST be omitted.
88            */
89            if (!empty($nameIdFormat) && $nameIdFormat == OneLogin_Saml2_Constants::NAMEID_ENTITY) {
90                $nameIdNameQualifier = null;
91                $nameIdSPNameQualifier = null;
92            }
93             // NameID Format UNSPECIFIED omitted
94            if (!empty($nameIdFormat) && $nameIdFormat == OneLogin_Saml2_Constants::NAMEID_UNSPECIFIED) {
95                $nameIdFormat = null;
96            }
97
98            $nameIdObj = OneLogin_Saml2_Utils::generateNameId(
99                $nameId,
100                $nameIdSPNameQualifier,
101                $nameIdFormat,
102                $cert,
103                $nameIdNameQualifier
104            );
105
106            $sessionIndexStr = isset($sessionIndex) ? "<samlp:SessionIndex>{$sessionIndex}</samlp:SessionIndex>" : "";
107
108            $spEntityId = htmlspecialchars($spData['entityId'], ENT_QUOTES);
109            $logoutRequest = <<<LOGOUTREQUEST
110<samlp:LogoutRequest
111    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
112    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
113    ID="{$id}"
114    Version="2.0"
115    IssueInstant="{$issueInstant}"
116    Destination="{$idpData['singleLogoutService']['url']}">
117    <saml:Issuer>{$spEntityId}</saml:Issuer>
118    {$nameIdObj}
119    {$sessionIndexStr}
120</samlp:LogoutRequest>
121LOGOUTREQUEST;
122        } else {
123            $decoded = base64_decode($request);
124            // We try to inflate
125            $inflated = @gzinflate($decoded);
126            if ($inflated != false) {
127                $logoutRequest = $inflated;
128            } else {
129                $logoutRequest = $decoded;
130            }
131            $this->id = self::getID($logoutRequest);
132        }
133        $this->_logoutRequest = $logoutRequest;
134    }
135
136
137    /**
138     * Returns the Logout Request defated, base64encoded, unsigned
139     *
140     * @param bool|null $deflate Whether or not we should 'gzdeflate' the request body before we return it.
141     *
142     * @return string Deflated base64 encoded Logout Request
143     */
144    public function getRequest($deflate = null)
145    {
146        $subject = $this->_logoutRequest;
147
148        if (is_null($deflate)) {
149            $deflate = $this->_settings->shouldCompressRequests();
150        }
151
152        if ($deflate) {
153            $subject = gzdeflate($this->_logoutRequest);
154        }
155
156        return base64_encode($subject);
157    }
158
159    /**
160     * Returns the ID of the Logout Request.
161     *
162     * @param string|DOMDocument $request Logout Request Message
163     *
164     * @return string ID
165     *
166     * @throws OneLogin_Saml2_Error
167     */
168    public static function getID($request)
169    {
170        if ($request instanceof DOMDocument) {
171            $dom = $request;
172        } else {
173            $dom = new DOMDocument();
174            $dom = OneLogin_Saml2_Utils::loadXML($dom, $request);
175
176            if (false === $dom) {
177                throw new OneLogin_Saml2_Error(
178                    "LogoutRequest could not be processed",
179                    OneLogin_Saml2_Error::SAML_LOGOUTREQUEST_INVALID
180                );
181            }
182        }
183
184        $id = $dom->documentElement->getAttribute('ID');
185        return $id;
186    }
187
188    /**
189     * Gets the NameID Data of the the Logout Request.
190     *
191     * @param string|DOMDocument $request Logout Request Message
192     * @param string|null $key The SP key
193     *
194     * @return array Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
195     *
196     * @throws OneLogin_Saml2_Error
197     * @throws OneLogin_Saml2_ValidationError
198     */
199    public static function getNameIdData($request, $key = null)
200    {
201        if ($request instanceof DOMDocument) {
202            $dom = $request;
203        } else {
204            $dom = new DOMDocument();
205            $dom = OneLogin_Saml2_Utils::loadXML($dom, $request);
206        }
207
208        $encryptedEntries = OneLogin_Saml2_Utils::query($dom, '/samlp:LogoutRequest/saml:EncryptedID');
209
210        if ($encryptedEntries->length == 1) {
211            $encryptedDataNodes = $encryptedEntries->item(0)->getElementsByTagName('EncryptedData');
212            $encryptedData = $encryptedDataNodes->item(0);
213
214            if (empty($key)) {
215                throw new OneLogin_Saml2_Error(
216                    "Private Key is required in order to decrypt the NameID, check settings",
217                    OneLogin_Saml2_Error::PRIVATE_KEY_NOT_FOUND
218                );
219            }
220
221            $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private'));
222            $seckey->loadKey($key);
223
224            $nameId = OneLogin_Saml2_Utils::decryptElement($encryptedData, $seckey);
225
226        } else {
227            $entries = OneLogin_Saml2_Utils::query($dom, '/samlp:LogoutRequest/saml:NameID');
228            if ($entries->length == 1) {
229                $nameId = $entries->item(0);
230            }
231        }
232
233        if (!isset($nameId)) {
234            throw new OneLogin_Saml2_ValidationError(
235                "NameID not found in the Logout Request",
236                OneLogin_Saml2_ValidationError::NO_NAMEID
237            );
238        }
239
240        $nameIdData = array();
241        $nameIdData['Value'] = $nameId->nodeValue;
242        foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) {
243            if ($nameId->hasAttribute($attr)) {
244                $nameIdData[$attr] = $nameId->getAttribute($attr);
245            }
246        }
247
248        return $nameIdData;
249    }
250
251    /**
252     * Gets the NameID of the Logout Request.
253     *
254     * @param string|DOMDocument $request Logout Request Message
255     * @param string|null $key The SP key
256     *
257     * @return string Name ID Value
258     *
259     * @throws OneLogin_Saml2_Error
260     * @throws OneLogin_Saml2_ValidationError
261     */
262    public static function getNameId($request, $key = null)
263    {
264        $nameId = self::getNameIdData($request, $key);
265        return $nameId['Value'];
266    }
267
268    /**
269     * Gets the Issuer of the Logout Request.
270     *
271     * @param string|DOMDocument $request Logout Request Message
272     *
273     * @return string|null $issuer The Issuer
274     * @throws Exception
275     */
276    public static function getIssuer($request)
277    {
278        if ($request instanceof DOMDocument) {
279            $dom = $request;
280        } else {
281            $dom = new DOMDocument();
282            $dom = OneLogin_Saml2_Utils::loadXML($dom, $request);
283        }
284
285        $issuer = null;
286        $issuerNodes = OneLogin_Saml2_Utils::query($dom, '/samlp:LogoutRequest/saml:Issuer');
287        if ($issuerNodes->length == 1) {
288            $issuer = $issuerNodes->item(0)->textContent;
289        }
290        return $issuer;
291    }
292
293    /**
294     * Gets the SessionIndexes from the Logout Request.
295     * Notice: Our Constructor only support 1 SessionIndex but this parser
296     *         extracts an array of all the  SessionIndex found on a
297     *         Logout Request, that could be many.
298     *
299     * @param string|DOMDocument $request Logout Request Message
300     *
301     * @return array The SessionIndex value
302     *
303     * @throws Exception
304     */
305    public static function getSessionIndexes($request)
306    {
307        if ($request instanceof DOMDocument) {
308            $dom = $request;
309        } else {
310            $dom = new DOMDocument();
311            $dom = OneLogin_Saml2_Utils::loadXML($dom, $request);
312        }
313
314        $sessionIndexes = array();
315        $sessionIndexNodes = OneLogin_Saml2_Utils::query($dom, '/samlp:LogoutRequest/samlp:SessionIndex');
316        foreach ($sessionIndexNodes as $sessionIndexNode) {
317            $sessionIndexes[] = $sessionIndexNode->textContent;
318        }
319        return $sessionIndexes;
320    }
321
322    /**
323     * Checks if the Logout Request recieved is valid.
324     *
325     * @param bool $retrieveParametersFromServer
326     *
327     * @return bool If the Logout Request is or not valid
328     */
329    public function isValid($retrieveParametersFromServer = false)
330    {
331        $this->_error = null;
332        try {
333            $dom = new DOMDocument();
334            $dom = OneLogin_Saml2_Utils::loadXML($dom, $this->_logoutRequest);
335
336            $idpData = $this->_settings->getIdPData();
337            $idPEntityId = $idpData['entityId'];
338
339            if ($this->_settings->isStrict()) {
340                $security = $this->_settings->getSecurityData();
341
342                if ($security['wantXMLValidation']) {
343                    $res = OneLogin_Saml2_Utils::validateXML($dom, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath());
344                    if (!$res instanceof DOMDocument) {
345                        throw new OneLogin_Saml2_ValidationError(
346                            "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd",
347                            OneLogin_Saml2_ValidationError::INVALID_XML_FORMAT
348                        );
349                    }
350                }
351
352                $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery();
353
354                // Check NotOnOrAfter
355                if ($dom->documentElement->hasAttribute('NotOnOrAfter')) {
356                    $na = OneLogin_Saml2_Utils::parseSAML2Time($dom->documentElement->getAttribute('NotOnOrAfter'));
357                    if ($na <= time()) {
358                        throw new OneLogin_Saml2_ValidationError(
359                            "Could not validate timestamp: expired. Check system clock.",
360                            OneLogin_Saml2_ValidationError::RESPONSE_EXPIRED
361                        );
362                    }
363                }
364
365                // Check destination
366                if ($dom->documentElement->hasAttribute('Destination')) {
367                    $destination = $dom->documentElement->getAttribute('Destination');
368                    if (empty($destination)) {
369                        if (!$security['relaxDestinationValidation']) {
370                            throw new OneLogin_Saml2_ValidationError(
371                                "The LogoutRequest has an empty Destination value",
372                                OneLogin_Saml2_ValidationError::EMPTY_DESTINATION
373                            );
374                        }
375                    } else {
376                        $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL);
377                        if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) {
378                            $currentURLNoRouted = OneLogin_Saml2_Utils::getSelfURLNoQuery();
379                            $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted);
380
381                            if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) {
382                                throw new OneLogin_Saml2_ValidationError(
383                                    "The LogoutRequest was received at $currentURL instead of $destination",
384                                    OneLogin_Saml2_ValidationError::WRONG_DESTINATION
385                                );
386                            }
387                        }
388                    }
389                }
390
391                $nameId = static::getNameId($dom, $this->_settings->getSPkey());
392
393                // Check issuer
394                $issuer = static::getIssuer($dom);
395                if (!empty($issuer) && $issuer != $idPEntityId) {
396                    throw new OneLogin_Saml2_ValidationError(
397                        "Invalid issuer in the Logout Request",
398                        OneLogin_Saml2_ValidationError::WRONG_ISSUER
399                    );
400                }
401
402                if ($security['wantMessagesSigned'] && !isset($_GET['Signature'])) {
403                    throw new OneLogin_Saml2_ValidationError(
404                        "The Message of the Logout Request is not signed and the SP require it",
405                        OneLogin_Saml2_ValidationError::NO_SIGNED_MESSAGE
406                    );
407                }
408            }
409
410            if (isset($_GET['Signature'])) {
411                $signatureValid = OneLogin_Saml2_Utils::validateBinarySign("SAMLRequest", $_GET, $idpData, $retrieveParametersFromServer);
412                if (!$signatureValid) {
413                    throw new OneLogin_Saml2_ValidationError(
414                        "Signature validation failed. Logout Request rejected",
415                        OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
416                    );
417                }
418            }
419
420            return true;
421        } catch (Exception $e) {
422            $this->_error = $e->getMessage();
423            $debug = $this->_settings->isDebugActive();
424            if ($debug) {
425                echo htmlentities($this->_error);
426            }
427            return false;
428        }
429    }
430
431    /**
432     * After execute a validation process, if fails this method returns the cause
433     *
434     * @return string Cause
435     */
436    public function getError()
437    {
438        return $this->_error;
439    }
440
441    /**
442     * Returns the XML that will be sent as part of the request
443     * or that was received at the SP
444     *
445     * @return string
446     */
447    public function getXML()
448    {
449        return $this->_logoutRequest;
450    }
451}
452