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