1<?php
2
3/**
4 * SAML 2 Logout Response
5 *
6 */
7class OneLogin_Saml2_LogoutResponse
8{
9    /**
10    * Contains the ID of the Logout Response
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     * The decoded, unprocessed XML response provided to the constructor.
23     * @var string
24     */
25    protected $_logoutResponse;
26
27    /**
28     * A DOMDocument class loaded from the SAML LogoutResponse.
29     * @var DomDocument
30     */
31    public $document;
32
33    /**
34    * After execute a validation process, if it fails, this var contains the cause
35    * @var string|null
36    */
37    private $_error;
38
39    /**
40     * Constructs a Logout Response object (Initialize params from settings and if provided
41     * load the Logout Response.
42     *
43     * @param OneLogin_Saml2_Settings $settings Settings.
44     * @param string|null             $response An UUEncoded SAML Logout response from the IdP.
45     *
46     * @throws OneLogin_Saml2_Error
47     */
48    public function __construct(OneLogin_Saml2_Settings $settings, $response = null)
49    {
50        $this->_settings = $settings;
51
52        $baseURL = $this->_settings->getBaseURL();
53        if (!empty($baseURL)) {
54            OneLogin_Saml2_Utils::setBaseURL($baseURL);
55        }
56
57        if ($response) {
58            $decoded = base64_decode($response);
59            $inflated = @gzinflate($decoded);
60            if ($inflated != false) {
61                $this->_logoutResponse = $inflated;
62            } else {
63                $this->_logoutResponse = $decoded;
64            }
65            $this->document = new DOMDocument();
66            $this->document = OneLogin_Saml2_Utils::loadXML($this->document, $this->_logoutResponse);
67
68            if (false === $this->document) {
69                throw new OneLogin_Saml2_Error(
70                    "LogoutResponse could not be processed",
71                    OneLogin_Saml2_Error::SAML_LOGOUTRESPONSE_INVALID
72                );
73            }
74
75            if ($this->document->documentElement->hasAttribute('ID')) {
76                $this->id = $this->document->documentElement->getAttribute('ID');
77            }
78        }
79    }
80
81    /**
82     * Gets the Issuer of the Logout Response.
83     *
84     * @return string|null $issuer The Issuer
85     */
86    public function getIssuer()
87    {
88        $issuer = null;
89        $issuerNodes = $this->_query('/samlp:LogoutResponse/saml:Issuer');
90        if ($issuerNodes->length == 1) {
91            $issuer = $issuerNodes->item(0)->textContent;
92        }
93        return $issuer;
94    }
95
96    /**
97     * Gets the Status of the Logout Response.
98     *
99     * @return string|null The Status
100     */
101    public function getStatus()
102    {
103        $entries = $this->_query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode');
104        if ($entries->length != 1) {
105            return null;
106        }
107        $status = $entries->item(0)->getAttribute('Value');
108        return $status;
109    }
110
111    /**
112     * Determines if the SAML LogoutResponse is valid
113     *
114     * @param string|null $requestId The ID of the LogoutRequest sent by this SP to the IdP
115     * @param bool $retrieveParametersFromServer
116     *
117     * @return bool Returns if the SAML LogoutResponse is or not valid
118     */
119    public function isValid($requestId = null, $retrieveParametersFromServer = false)
120    {
121        $this->_error = null;
122        try {
123            $idpData = $this->_settings->getIdPData();
124            $idPEntityId = $idpData['entityId'];
125
126            if ($this->_settings->isStrict()) {
127                $security = $this->_settings->getSecurityData();
128
129                if ($security['wantXMLValidation']) {
130                    $res = OneLogin_Saml2_Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath());
131                    if (!$res instanceof DOMDocument) {
132                        throw new OneLogin_Saml2_ValidationError(
133                            "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd",
134                            OneLogin_Saml2_ValidationError::INVALID_XML_FORMAT
135                        );
136                    }
137                }
138
139                // Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided
140                if (isset($requestId) && $this->document->documentElement->hasAttribute('InResponseTo')) {
141                    $inResponseTo = $this->document->documentElement->getAttribute('InResponseTo');
142                    if ($requestId != $inResponseTo) {
143                        throw new OneLogin_Saml2_ValidationError(
144                            "The InResponseTo of the Logout Response: $inResponseTo, does not match the ID of the Logout request sent by the SP: $requestId",
145                            OneLogin_Saml2_ValidationError::WRONG_INRESPONSETO
146                        );
147                    }
148                }
149
150                // Check issuer
151                $issuer = $this->getIssuer();
152                if (!empty($issuer) && $issuer != $idPEntityId) {
153                    throw new OneLogin_Saml2_ValidationError(
154                        "Invalid issuer in the Logout Response",
155                        OneLogin_Saml2_ValidationError::WRONG_ISSUER
156                    );
157                }
158
159                $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery();
160
161                // Check destination
162                if ($this->document->documentElement->hasAttribute('Destination')) {
163                    $destination = $this->document->documentElement->getAttribute('Destination');
164                    if (empty($destination)) {
165                        if (!$security['relaxDestinationValidation']) {
166                            throw new OneLogin_Saml2_ValidationError(
167                                "The LogoutResponse has an empty Destination value",
168                                OneLogin_Saml2_ValidationError::EMPTY_DESTINATION
169                            );
170                        }
171                    } else {
172                        $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL);
173                        if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) {
174                            $currentURLNoRouted = OneLogin_Saml2_Utils::getSelfURLNoQuery();
175                            $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted);
176
177                            if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) {
178                                throw new OneLogin_Saml2_ValidationError(
179                                    "The LogoutResponse was received at $currentURL instead of $destination",
180                                    OneLogin_Saml2_ValidationError::WRONG_DESTINATION
181                                );
182                            }
183                        }
184                    }
185                }
186
187                if ($security['wantMessagesSigned'] && !isset($_GET['Signature'])) {
188                    throw new OneLogin_Saml2_ValidationError(
189                        "The Message of the Logout Response is not signed and the SP requires it",
190                        OneLogin_Saml2_ValidationError::NO_SIGNED_MESSAGE
191                    );
192                }
193            }
194
195            if (isset($_GET['Signature'])) {
196                $signatureValid = OneLogin_Saml2_Utils::validateBinarySign("SAMLResponse", $_GET, $idpData, $retrieveParametersFromServer);
197                if (!$signatureValid) {
198                    throw new OneLogin_Saml2_ValidationError(
199                        "Signature validation failed. Logout Response rejected",
200                        OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
201                    );
202                }
203            }
204            return true;
205        } catch (Exception $e) {
206            $this->_error = $e->getMessage();
207            $debug = $this->_settings->isDebugActive();
208            if ($debug) {
209                echo htmlentities($this->_error);
210            }
211            return false;
212        }
213    }
214
215    /**
216     * Extracts a node from the DOMDocument (Logout Response Menssage)
217     *
218     * @param string $query Xpath Expresion
219     *
220     * @return DOMNodeList The queried node
221     */
222    private function _query($query)
223    {
224        return OneLogin_Saml2_Utils::query($this->document, $query);
225
226    }
227
228    /**
229     * Generates a Logout Response object.
230     *
231     * @param string $inResponseTo InResponseTo value for the Logout Response.
232     */
233    public function build($inResponseTo)
234    {
235
236        $spData = $this->_settings->getSPData();
237        $idpData = $this->_settings->getIdPData();
238
239        $this->id = OneLogin_Saml2_Utils::generateUniqueID();
240        $issueInstant = OneLogin_Saml2_Utils::parseTime2SAML(time());
241
242        $spEntityId = htmlspecialchars($spData['entityId'], ENT_QUOTES);
243        $logoutResponse = <<<LOGOUTRESPONSE
244<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
245                  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
246                  ID="{$this->id}"
247                  Version="2.0"
248                  IssueInstant="{$issueInstant}"
249                  Destination="{$idpData['singleLogoutService']['url']}"
250                  InResponseTo="{$inResponseTo}"
251                  >
252    <saml:Issuer>{$spEntityId}</saml:Issuer>
253    <samlp:Status>
254        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
255    </samlp:Status>
256</samlp:LogoutResponse>
257LOGOUTRESPONSE;
258        $this->_logoutResponse = $logoutResponse;
259    }
260
261    /**
262     * Returns a Logout Response object.
263     *
264     * @param bool|null $deflate Whether or not we should 'gzdeflate' the response body before we return it.
265     *
266     * @return string Logout Response deflated and base64 encoded
267     */
268    public function getResponse($deflate = null)
269    {
270        $subject = $this->_logoutResponse;
271
272        if (is_null($deflate)) {
273            $deflate = $this->_settings->shouldCompressResponses();
274        }
275
276        if ($deflate) {
277            $subject = gzdeflate($this->_logoutResponse);
278        }
279        return base64_encode($subject);
280    }
281
282    /**
283     * After execute a validation process, if fails this method returns the cause.
284     *
285     * @return string Cause
286     */
287    public function getError()
288    {
289        return $this->_error;
290    }
291
292   /**
293    * @return string the ID of the Response
294    */
295    public function getId()
296    {
297        return $this->id;
298    }
299
300    /**
301     * Returns the XML that will be sent as part of the response
302     * or that was received at the SP
303     *
304     * @return string
305     */
306    public function getXML()
307    {
308        return $this->_logoutResponse;
309    }
310}
311