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