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