1<?php 2 3/** 4 * SAML 2 Authentication Response 5 * 6 */ 7 8class OneLogin_Saml2_Response 9{ 10 11 /** 12 * Settings 13 * @var OneLogin_Saml2_Settings 14 */ 15 protected $_settings; 16 17 /** 18 * The decoded, unprocessed XML response provided to the constructor. 19 * @var string 20 */ 21 public $response; 22 23 /** 24 * A DOMDocument class loaded from the SAML Response. 25 * @var DomDocument 26 */ 27 public $document; 28 29 /** 30 * A DOMDocument class loaded from the SAML Response (Decrypted). 31 * @var DomDocument 32 */ 33 public $decryptedDocument; 34 35 /** 36 * The response contains an encrypted assertion. 37 * @var bool 38 */ 39 public $encrypted = false; 40 41 /** 42 * After validation, if it fail this var has the cause of the problem 43 * @var string 44 */ 45 private $_error; 46 47 /** 48 * NotOnOrAfter value of a valid SubjectConfirmationData node 49 * 50 * @var int 51 */ 52 private $_validSCDNotOnOrAfter; 53 54 /** 55 * Constructs the SAML Response object. 56 * 57 * @param OneLogin_Saml2_Settings $settings Settings. 58 * @param string $response A UUEncoded SAML response from the IdP. 59 * 60 * @throws OneLogin_Saml2_Error 61 * @throws OneLogin_Saml2_ValidationError 62 */ 63 public function __construct(OneLogin_Saml2_Settings $settings, $response) 64 { 65 $this->_settings = $settings; 66 67 $baseURL = $this->_settings->getBaseURL(); 68 if (!empty($baseURL)) { 69 OneLogin_Saml2_Utils::setBaseURL($baseURL); 70 } 71 72 $this->response = base64_decode($response); 73 74 $this->document = new DOMDocument(); 75 $this->document = OneLogin_Saml2_Utils::loadXML($this->document, $this->response); 76 if (!$this->document) { 77 throw new OneLogin_Saml2_ValidationError( 78 "SAML Response could not be processed", 79 OneLogin_Saml2_ValidationError::INVALID_XML_FORMAT 80 ); 81 } 82 83 // Quick check for the presence of EncryptedAssertion 84 $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion'); 85 if ($encryptedAssertionNodes->length !== 0) { 86 $this->decryptedDocument = clone $this->document; 87 $this->encrypted = true; 88 $this->decryptedDocument = $this->_decryptAssertion($this->decryptedDocument); 89 } 90 } 91 92 /** 93 * Determines if the SAML Response is valid using the certificate. 94 * 95 * @param string|null $requestId The ID of the AuthNRequest sent by this SP to the IdP 96 * 97 * @return bool Validate the document 98 */ 99 public function isValid($requestId = null) 100 { 101 $this->_error = null; 102 try { 103 // Check SAML version 104 if ($this->document->documentElement->getAttribute('Version') != '2.0') { 105 throw new OneLogin_Saml2_ValidationError( 106 "Unsupported SAML version", 107 OneLogin_Saml2_ValidationError::UNSUPPORTED_SAML_VERSION 108 ); 109 } 110 111 if (!$this->document->documentElement->hasAttribute('ID')) { 112 throw new OneLogin_Saml2_ValidationError( 113 "Missing ID attribute on SAML Response", 114 OneLogin_Saml2_ValidationError::MISSING_ID 115 ); 116 } 117 118 $this->checkStatus(); 119 120 $singleAssertion = $this->validateNumAssertions(); 121 if (!$singleAssertion) { 122 throw new OneLogin_Saml2_ValidationError( 123 "SAML Response must contain 1 assertion", 124 OneLogin_Saml2_ValidationError::WRONG_NUMBER_OF_ASSERTIONS 125 ); 126 } 127 128 $idpData = $this->_settings->getIdPData(); 129 $idPEntityId = $idpData['entityId']; 130 $spData = $this->_settings->getSPData(); 131 $spEntityId = $spData['entityId']; 132 133 $signedElements = $this->processSignedElements(); 134 135 $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response'; 136 $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion'; 137 138 $hasSignedResponse = in_array($responseTag, $signedElements); 139 $hasSignedAssertion = in_array($assertionTag, $signedElements); 140 141 if ($this->_settings->isStrict()) { 142 $security = $this->_settings->getSecurityData(); 143 144 if ($security['wantXMLValidation']) { 145 $errorXmlMsg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"; 146 $res = OneLogin_Saml2_Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); 147 if (!$res instanceof DOMDocument) { 148 throw new OneLogin_Saml2_ValidationError( 149 $errorXmlMsg, 150 OneLogin_Saml2_ValidationError::INVALID_XML_FORMAT 151 ); 152 } 153 154 # If encrypted, check also the decrypted document 155 if ($this->encrypted) { 156 $res = OneLogin_Saml2_Utils::validateXML($this->decryptedDocument, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); 157 if (!$res instanceof DOMDocument) { 158 throw new OneLogin_Saml2_ValidationError( 159 $errorXmlMsg, 160 OneLogin_Saml2_ValidationError::INVALID_XML_FORMAT 161 ); 162 } 163 } 164 165 } 166 167 $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery(); 168 169 $responseInResponseTo = null; 170 if ($this->document->documentElement->hasAttribute('InResponseTo')) { 171 $responseInResponseTo = $this->document->documentElement->getAttribute('InResponseTo'); 172 } 173 174 if (!isset($requestId) && isset($responseInResponseTo) && $security['rejectUnsolicitedResponsesWithInResponseTo']) { 175 throw new OneLogin_Saml2_ValidationError( 176 "The Response has an InResponseTo attribute: " . $responseInResponseTo . " while no InResponseTo was expected", 177 OneLogin_Saml2_ValidationError::WRONG_INRESPONSETO 178 ); 179 } 180 181 // Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided 182 if (isset($requestId) && $requestId != $responseInResponseTo) { 183 if ($responseInResponseTo == null) { 184 throw new OneLogin_Saml2_ValidationError( 185 "No InResponseTo at the Response, but it was provided the requestId related to the AuthNRequest sent by the SP: $requestId", 186 OneLogin_Saml2_ValidationError::WRONG_INRESPONSETO 187 ); 188 } else { 189 throw new OneLogin_Saml2_ValidationError( 190 "The InResponseTo of the Response: $responseInResponseTo, does not match the ID of the AuthNRequest sent by the SP: $requestId", 191 OneLogin_Saml2_ValidationError::WRONG_INRESPONSETO 192 ); 193 } 194 } 195 196 if (!$this->encrypted && $security['wantAssertionsEncrypted']) { 197 throw new OneLogin_Saml2_ValidationError( 198 "The assertion of the Response is not encrypted and the SP requires it", 199 OneLogin_Saml2_ValidationError::NO_ENCRYPTED_ASSERTION 200 ); 201 } 202 203 if ($security['wantNameIdEncrypted']) { 204 $encryptedIdNodes = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData'); 205 if ($encryptedIdNodes->length != 1) { 206 throw new OneLogin_Saml2_ValidationError( 207 "The NameID of the Response is not encrypted and the SP requires it", 208 OneLogin_Saml2_ValidationError::NO_ENCRYPTED_NAMEID 209 ); 210 } 211 } 212 213 // Validate Conditions element exists 214 if (!$this->checkOneCondition()) { 215 throw new OneLogin_Saml2_ValidationError( 216 "The Assertion must include a Conditions element", 217 OneLogin_Saml2_ValidationError::MISSING_CONDITIONS 218 ); 219 } 220 221 // Validate Asserion timestamps 222 $this->validateTimestamps(); 223 224 // Validate AuthnStatement element exists and is unique 225 if (!$this->checkOneAuthnStatement()) { 226 throw new OneLogin_Saml2_ValidationError( 227 "The Assertion must include an AuthnStatement element", 228 OneLogin_Saml2_ValidationError::WRONG_NUMBER_OF_AUTHSTATEMENTS 229 ); 230 } 231 232 // EncryptedAttributes are not supported 233 $encryptedAttributeNodes = $this->_queryAssertion('/saml:AttributeStatement/saml:EncryptedAttribute'); 234 if ($encryptedAttributeNodes->length > 0) { 235 throw new OneLogin_Saml2_ValidationError( 236 "There is an EncryptedAttribute in the Response and this SP not support them", 237 OneLogin_Saml2_ValidationError::ENCRYPTED_ATTRIBUTES 238 ); 239 } 240 241 // Check destination 242 if ($this->document->documentElement->hasAttribute('Destination')) { 243 $destination = trim($this->document->documentElement->getAttribute('Destination')); 244 if (empty($destination)) { 245 if (!$security['relaxDestinationValidation']) { 246 throw new OneLogin_Saml2_ValidationError( 247 "The response has an empty Destination value", 248 OneLogin_Saml2_ValidationError::EMPTY_DESTINATION 249 ); 250 } 251 } else { 252 $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL); 253 if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) { 254 $currentURLNoRouted = OneLogin_Saml2_Utils::getSelfURLNoQuery(); 255 $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted); 256 257 if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) { 258 throw new OneLogin_Saml2_ValidationError( 259 "The response was received at $currentURL instead of $destination", 260 OneLogin_Saml2_ValidationError::WRONG_DESTINATION 261 ); 262 } 263 } 264 } 265 } 266 267 // Check audience 268 $validAudiences = $this->getAudiences(); 269 if (!empty($validAudiences) && !in_array($spEntityId, $validAudiences, true)) { 270 throw new OneLogin_Saml2_ValidationError( 271 sprintf( 272 "Invalid audience for this Response (expected '%s', got '%s')", 273 $spEntityId, 274 implode(',', $validAudiences) 275 ), 276 OneLogin_Saml2_ValidationError::WRONG_AUDIENCE 277 ); 278 } 279 280 // Check the issuers 281 $issuers = $this->getIssuers(); 282 foreach ($issuers as $issuer) { 283 $trimmedIssuer = trim($issuer); 284 if (empty($trimmedIssuer) || $trimmedIssuer !== $idPEntityId) { 285 throw new OneLogin_Saml2_ValidationError( 286 "Invalid issuer in the Assertion/Response (expected '$idPEntityId', got '$trimmedIssuer')", 287 OneLogin_Saml2_ValidationError::WRONG_ISSUER 288 ); 289 } 290 } 291 292 // Check the session Expiration 293 $sessionExpiration = $this->getSessionNotOnOrAfter(); 294 if (!empty($sessionExpiration) && $sessionExpiration + OneLogin_Saml2_Constants::ALLOWED_CLOCK_DRIFT <= time()) { 295 throw new OneLogin_Saml2_ValidationError( 296 "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response", 297 OneLogin_Saml2_ValidationError::SESSION_EXPIRED 298 ); 299 } 300 301 // Check the SubjectConfirmation, at least one SubjectConfirmation must be valid 302 $anySubjectConfirmation = false; 303 $subjectConfirmationNodes = $this->_queryAssertion('/saml:Subject/saml:SubjectConfirmation'); 304 foreach ($subjectConfirmationNodes as $scn) { 305 if ($scn->hasAttribute('Method') && $scn->getAttribute('Method') != OneLogin_Saml2_Constants::CM_BEARER) { 306 continue; 307 } 308 $subjectConfirmationDataNodes = $scn->getElementsByTagName('SubjectConfirmationData'); 309 if ($subjectConfirmationDataNodes->length == 0) { 310 continue; 311 } else { 312 $scnData = $subjectConfirmationDataNodes->item(0); 313 if ($scnData->hasAttribute('InResponseTo')) { 314 $inResponseTo = $scnData->getAttribute('InResponseTo'); 315 if (isset($responseInResponseTo) && $responseInResponseTo != $inResponseTo) { 316 continue; 317 } 318 } 319 if ($scnData->hasAttribute('Recipient')) { 320 $recipient = $scnData->getAttribute('Recipient'); 321 if (!empty($recipient) && strpos($recipient, $currentURL) === false) { 322 continue; 323 } 324 } 325 if ($scnData->hasAttribute('NotOnOrAfter')) { 326 $noa = OneLogin_Saml2_Utils::parseSAML2Time($scnData->getAttribute('NotOnOrAfter')); 327 if ($noa + OneLogin_Saml2_Constants::ALLOWED_CLOCK_DRIFT <= time()) { 328 continue; 329 } 330 } 331 if ($scnData->hasAttribute('NotBefore')) { 332 $nb = OneLogin_Saml2_Utils::parseSAML2Time($scnData->getAttribute('NotBefore')); 333 if ($nb > time() + OneLogin_Saml2_Constants::ALLOWED_CLOCK_DRIFT) { 334 continue; 335 } 336 } 337 338 // Save NotOnOrAfter value 339 if ($scnData->hasAttribute('NotOnOrAfter')) { 340 $this->_validSCDNotOnOrAfter = $noa; 341 } 342 $anySubjectConfirmation = true; 343 break; 344 } 345 } 346 347 if (!$anySubjectConfirmation) { 348 throw new OneLogin_Saml2_ValidationError( 349 "A valid SubjectConfirmation was not found on this Response", 350 OneLogin_Saml2_ValidationError::WRONG_SUBJECTCONFIRMATION 351 ); 352 } 353 354 if ($security['wantAssertionsSigned'] && !$hasSignedAssertion) { 355 throw new OneLogin_Saml2_ValidationError( 356 "The Assertion of the Response is not signed and the SP requires it", 357 OneLogin_Saml2_ValidationError::NO_SIGNED_ASSERTION 358 ); 359 } 360 361 if ($security['wantMessagesSigned'] && !$hasSignedResponse) { 362 throw new OneLogin_Saml2_ValidationError( 363 "The Message of the Response is not signed and the SP requires it", 364 OneLogin_Saml2_ValidationError::NO_SIGNED_MESSAGE 365 ); 366 } 367 } 368 369 // Detect case not supported 370 if ($this->encrypted) { 371 $encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID'); 372 if ($encryptedIDNodes->length > 0) { 373 throw new OneLogin_Saml2_ValidationError( 374 'SAML Response that contains a an encrypted Assertion with encrypted nameId is not supported.', 375 OneLogin_Saml2_ValidationError::NOT_SUPPORTED 376 ); 377 } 378 } 379 380 if (empty($signedElements) || (!$hasSignedResponse && !$hasSignedAssertion)) { 381 throw new OneLogin_Saml2_ValidationError( 382 'No Signature found. SAML Response rejected', 383 OneLogin_Saml2_ValidationError::NO_SIGNATURE_FOUND 384 ); 385 } else { 386 $cert = $idpData['x509cert']; 387 $fingerprint = $idpData['certFingerprint']; 388 $fingerprintalg = $idpData['certFingerprintAlgorithm']; 389 390 $multiCerts = null; 391 $existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']); 392 393 if ($existsMultiX509Sign) { 394 $multiCerts = $idpData['x509certMulti']['signing']; 395 } 396 397 # If find a Signature on the Response, validates it checking the original response 398 if ($hasSignedResponse && !OneLogin_Saml2_Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH, $multiCerts)) { 399 throw new OneLogin_Saml2_ValidationError( 400 "Signature validation failed. SAML Response rejected", 401 OneLogin_Saml2_ValidationError::INVALID_SIGNATURE 402 ); 403 } 404 405 # If find a Signature on the Assertion (decrypted assertion if was encrypted) 406 $documentToCheckAssertion = $this->encrypted ? $this->decryptedDocument : $this->document; 407 if ($hasSignedAssertion && !OneLogin_Saml2_Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH, $multiCerts)) { 408 throw new OneLogin_Saml2_ValidationError( 409 "Signature validation failed. SAML Response rejected", 410 OneLogin_Saml2_ValidationError::INVALID_SIGNATURE 411 ); 412 } 413 } 414 return true; 415 } catch (Exception $e) { 416 $this->_error = $e->getMessage(); 417 $debug = $this->_settings->isDebugActive(); 418 if ($debug) { 419 echo htmlentities($this->_error); 420 } 421 return false; 422 } 423 } 424 425 /** 426 * @return string|null the ID of the Response 427 */ 428 public function getId() 429 { 430 $id = null; 431 if ($this->document->documentElement->hasAttribute('ID')) { 432 $id = $this->document->documentElement->getAttribute('ID'); 433 } 434 return $id; 435 } 436 437 /** 438 * @return string|null the ID of the assertion in the Response 439 * 440 * @throws InvalidArgumentException 441 */ 442 public function getAssertionId() 443 { 444 if (!$this->validateNumAssertions()) { 445 throw new InvalidArgumentException("SAML Response must contain 1 Assertion."); 446 } 447 $assertionNodes = $this->_queryAssertion(""); 448 $id = null; 449 if ($assertionNodes->length == 1 && $assertionNodes->item(0)->hasAttribute('ID')) { 450 $id = $assertionNodes->item(0)->getAttribute('ID'); 451 } 452 return $id; 453 } 454 455 /** 456 * @return int the NotOnOrAfter value of the valid SubjectConfirmationData 457 * node if any 458 */ 459 public function getAssertionNotOnOrAfter() 460 { 461 return $this->_validSCDNotOnOrAfter; 462 } 463 464 /** 465 * Checks if the Status is success 466 * 467 * @throws OneLogin_Saml2_ValidationError If status is not success 468 */ 469 public function checkStatus() 470 { 471 $status = OneLogin_Saml2_Utils::getStatus($this->document); 472 473 if (isset($status['code']) && $status['code'] !== OneLogin_Saml2_Constants::STATUS_SUCCESS) { 474 $explodedCode = explode(':', $status['code']); 475 $printableCode = array_pop($explodedCode); 476 477 $statusExceptionMsg = 'The status code of the Response was not Success, was '.$printableCode; 478 if (!empty($status['msg'])) { 479 $statusExceptionMsg .= ' -> '.$status['msg']; 480 } 481 throw new OneLogin_Saml2_ValidationError( 482 $statusExceptionMsg, 483 OneLogin_Saml2_ValidationError::STATUS_CODE_IS_NOT_SUCCESS 484 ); 485 } 486 } 487 488 /** 489 * Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. 490 * 491 * @return boolean true if the Conditions element exists and is unique 492 */ 493 public function checkOneCondition() 494 { 495 $entries = $this->_queryAssertion("/saml:Conditions"); 496 if ($entries->length == 1) { 497 return true; 498 } else { 499 return false; 500 } 501 } 502 503 /** 504 * Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. 505 * 506 * @return boolean true if the AuthnStatement element exists and is unique 507 */ 508 public function checkOneAuthnStatement() 509 { 510 $entries = $this->_queryAssertion("/saml:AuthnStatement"); 511 if ($entries->length == 1) { 512 return true; 513 } else { 514 return false; 515 } 516 } 517 518 /** 519 * Gets the audiences. 520 * 521 * @return array @audience The valid audiences of the response 522 */ 523 public function getAudiences() 524 { 525 $audiences = array(); 526 527 $entries = $this->_queryAssertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience'); 528 foreach ($entries as $entry) { 529 $value = trim($entry->textContent); 530 if (!empty($value)) { 531 $audiences[] = $value; 532 } 533 } 534 535 return array_unique($audiences); 536 } 537 538 /** 539 * Gets the Issuers (from Response and Assertion). 540 * 541 * @return array @issuers The issuers of the assertion/response 542 * 543 * @throws OneLogin_Saml2_ValidationError 544 */ 545 public function getIssuers() 546 { 547 $issuers = array(); 548 549 $responseIssuer = OneLogin_Saml2_Utils::query($this->document, '/samlp:Response/saml:Issuer'); 550 if ($responseIssuer->length > 0) { 551 if ($responseIssuer->length == 1) { 552 $issuers[] = $responseIssuer->item(0)->textContent; 553 } else { 554 throw new OneLogin_Saml2_ValidationError( 555 "Issuer of the Response is multiple.", 556 OneLogin_Saml2_ValidationError::ISSUER_MULTIPLE_IN_RESPONSE 557 ); 558 } 559 } 560 561 $assertionIssuer = $this->_queryAssertion('/saml:Issuer'); 562 if ($assertionIssuer->length == 1) { 563 $issuers[] = $assertionIssuer->item(0)->textContent; 564 } else { 565 throw new OneLogin_Saml2_ValidationError( 566 "Issuer of the Assertion not found or multiple.", 567 OneLogin_Saml2_ValidationError::ISSUER_NOT_FOUND_IN_ASSERTION 568 ); 569 } 570 571 return array_unique($issuers); 572 } 573 574 /** 575 * Gets the NameID Data provided by the SAML response from the IdP. 576 * 577 * @return array Name ID Data (Value, Format, NameQualifier, SPNameQualifier) 578 * 579 * @throws OneLogin_Saml2_ValidationError 580 */ 581 public function getNameIdData() 582 { 583 $encryptedIdDataEntries = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData'); 584 585 if ($encryptedIdDataEntries->length == 1) { 586 $encryptedData = $encryptedIdDataEntries->item(0); 587 588 $key = $this->_settings->getSPkey(); 589 $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private')); 590 $seckey->loadKey($key); 591 592 $nameId = OneLogin_Saml2_Utils::decryptElement($encryptedData, $seckey); 593 594 } else { 595 $entries = $this->_queryAssertion('/saml:Subject/saml:NameID'); 596 if ($entries->length == 1) { 597 $nameId = $entries->item(0); 598 } 599 } 600 601 $nameIdData = array(); 602 603 if (!isset($nameId)) { 604 $security = $this->_settings->getSecurityData(); 605 if ($security['wantNameId']) { 606 throw new OneLogin_Saml2_ValidationError( 607 "NameID not found in the assertion of the Response", 608 OneLogin_Saml2_ValidationError::NO_NAMEID 609 ); 610 } 611 } else { 612 if ($this->_settings->isStrict() && empty($nameId->nodeValue)) { 613 throw new OneLogin_Saml2_ValidationError( 614 "An empty NameID value found", 615 OneLogin_Saml2_ValidationError::EMPTY_NAMEID 616 ); 617 } 618 $nameIdData['Value'] = $nameId->nodeValue; 619 620 foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) { 621 if ($nameId->hasAttribute($attr)) { 622 if ($this->_settings->isStrict() && $attr == 'SPNameQualifier') { 623 $spData = $this->_settings->getSPData(); 624 $spEntityId = $spData['entityId']; 625 if ($spEntityId != $nameId->getAttribute($attr)) { 626 throw new OneLogin_Saml2_ValidationError( 627 "The SPNameQualifier value mistmatch the SP entityID value.", 628 OneLogin_Saml2_ValidationError::SP_NAME_QUALIFIER_NAME_MISMATCH 629 ); 630 } 631 } 632 $nameIdData[$attr] = $nameId->getAttribute($attr); 633 } 634 } 635 } 636 637 return $nameIdData; 638 } 639 640 /** 641 * Gets the NameID provided by the SAML response from the IdP. 642 * 643 * @return string|null Name ID Value 644 * 645 * @throws OneLogin_Saml2_ValidationError 646 */ 647 public function getNameId() 648 { 649 $nameIdvalue = null; 650 $nameIdData = $this->getNameIdData(); 651 if (!empty($nameIdData) && isset($nameIdData['Value'])) { 652 $nameIdvalue = $nameIdData['Value']; 653 } 654 return $nameIdvalue; 655 } 656 657 /** 658 * Gets the NameID Format provided by the SAML response from the IdP. 659 * 660 * @return string|null Name ID Format 661 * 662 * @throws OneLogin_Saml2_ValidationError 663 */ 664 public function getNameIdFormat() 665 { 666 $nameIdFormat = null; 667 $nameIdData = $this->getNameIdData(); 668 if (!empty($nameIdData) && isset($nameIdData['Format'])) { 669 $nameIdFormat = $nameIdData['Format']; 670 } 671 return $nameIdFormat; 672 } 673 674 /** 675 * Gets the NameID NameQualifier provided by the SAML response from the IdP. 676 * 677 * @return string|null Name ID NameQualifier 678 * 679 * @throws OneLogin_Saml2_ValidationError 680 */ 681 public function getNameIdNameQualifier() 682 { 683 $nameIdNameQualifier = null; 684 $nameIdData = $this->getNameIdData(); 685 if (!empty($nameIdData) && isset($nameIdData['NameQualifier'])) { 686 $nameIdNameQualifier = $nameIdData['NameQualifier']; 687 } 688 return $nameIdNameQualifier; 689 } 690 691 /** 692 * Gets the NameID SP NameQualifier provided by the SAML response from the IdP. 693 * 694 * @return string|null NameID SP NameQualifier 695 * 696 * @throws ValidationError 697 */ 698 public function getNameIdSPNameQualifier() 699 { 700 $nameIdSPNameQualifier = null; 701 $nameIdData = $this->getNameIdData(); 702 if (!empty($nameIdData) && isset($nameIdData['SPNameQualifier'])) { 703 $nameIdSPNameQualifier = $nameIdData['SPNameQualifier']; 704 } 705 return $nameIdSPNameQualifier; 706 } 707 708 /** 709 * Gets the SessionNotOnOrAfter from the AuthnStatement. 710 * Could be used to set the local session expiration 711 * 712 * @return int|null The SessionNotOnOrAfter value 713 * 714 * @throws Exception 715 */ 716 public function getSessionNotOnOrAfter() 717 { 718 $notOnOrAfter = null; 719 $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionNotOnOrAfter]'); 720 if ($entries->length !== 0) { 721 $notOnOrAfter = OneLogin_Saml2_Utils::parseSAML2Time($entries->item(0)->getAttribute('SessionNotOnOrAfter')); 722 } 723 return $notOnOrAfter; 724 } 725 726 /** 727 * Gets the SessionIndex from the AuthnStatement. 728 * Could be used to be stored in the local session in order 729 * to be used in a future Logout Request that the SP could 730 * send to the SP, to set what specific session must be deleted 731 * 732 * @return string|null The SessionIndex value 733 */ 734 735 public function getSessionIndex() 736 { 737 $sessionIndex = null; 738 $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionIndex]'); 739 if ($entries->length !== 0) { 740 $sessionIndex = $entries->item(0)->getAttribute('SessionIndex'); 741 } 742 return $sessionIndex; 743 } 744 745 /** 746 * Gets the Attributes from the AttributeStatement element. 747 * 748 * @return array The attributes of the SAML Assertion 749 * 750 * @throws OneLogin_Saml2_ValidationError 751 */ 752 public function getAttributes() 753 { 754 return $this->_getAttributesByKeyName('Name'); 755 } 756 757 /** 758 * Gets the Attributes from the AttributeStatement element using their FriendlyName. 759 * 760 * @return array The attributes of the SAML Assertion 761 * 762 * @throws OneLogin_Saml2_ValidationError 763 */ 764 public function getAttributesWithFriendlyName() 765 { 766 return $this->_getAttributesByKeyName('FriendlyName'); 767 } 768 769 /** 770 * @param string $keyName 771 * 772 * @return array 773 * 774 * @throws OneLogin_Saml2_ValidationError 775 */ 776 private function _getAttributesByKeyName($keyName = "Name") 777 { 778 $attributes = array(); 779 780 $entries = $this->_queryAssertion('/saml:AttributeStatement/saml:Attribute'); 781 782 /** @var $entry DOMNode */ 783 foreach ($entries as $entry) { 784 $attributeKeyNode = $entry->attributes->getNamedItem($keyName); 785 786 if ($attributeKeyNode === null) { 787 continue; 788 } 789 790 $attributeKeyName = $attributeKeyNode->nodeValue; 791 792 if (in_array($attributeKeyName, array_keys($attributes))) { 793 throw new OneLogin_Saml2_ValidationError( 794 "Found an Attribute element with duplicated ".$keyName, 795 OneLogin_Saml2_ValidationError::DUPLICATED_ATTRIBUTE_NAME_FOUND 796 ); 797 } 798 799 $attributeValues = array(); 800 foreach ($entry->childNodes as $childNode) { 801 $tagName = ($childNode->prefix ? $childNode->prefix.':' : '') . 'AttributeValue'; 802 if ($childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName === $tagName) { 803 $attributeValues[] = $childNode->nodeValue; 804 } 805 } 806 807 $attributes[$attributeKeyName] = $attributeValues; 808 } 809 return $attributes; 810 } 811 812 /** 813 * Verifies that the document only contains a single Assertion (encrypted or not). 814 * 815 * @return bool TRUE if the document passes. 816 */ 817 public function validateNumAssertions() 818 { 819 $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion'); 820 $assertionNodes = $this->document->getElementsByTagName('Assertion'); 821 822 $valid = $assertionNodes->length + $encryptedAssertionNodes->length == 1; 823 824 if ($this->encrypted) { 825 $assertionNodes = $this->decryptedDocument->getElementsByTagName('Assertion'); 826 $valid = $valid && $assertionNodes->length == 1; 827 } 828 829 return $valid; 830 } 831 832 /** 833 * Verifies the signature nodes: 834 * - Checks that are Response or Assertion 835 * - Check that IDs and reference URI are unique and consistent. 836 * 837 * @return array Signed element tags 838 * 839 * @throws OneLogin_Saml2_ValidationError 840 */ 841 public function processSignedElements() 842 { 843 $signedElements = array(); 844 $verifiedSeis = array(); 845 $verifiedIds = array(); 846 847 if ($this->encrypted) { 848 $signNodes = $this->decryptedDocument->getElementsByTagName('Signature'); 849 } else { 850 $signNodes = $this->document->getElementsByTagName('Signature'); 851 } 852 foreach ($signNodes as $signNode) { 853 $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response'; 854 $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion'; 855 856 $signedElement = '{'.$signNode->parentNode->namespaceURI.'}'.$signNode->parentNode->localName; 857 858 if ($signedElement != $responseTag && $signedElement != $assertionTag) { 859 throw new OneLogin_Saml2_ValidationError( 860 "Invalid Signature Element $signedElement SAML Response rejected", 861 OneLogin_Saml2_ValidationError::WRONG_SIGNED_ELEMENT 862 ); 863 } 864 865 # Check that reference URI matches the parent ID and no duplicate References or IDs 866 $idValue = $signNode->parentNode->getAttribute('ID'); 867 if (empty($idValue)) { 868 throw new OneLogin_Saml2_ValidationError( 869 'Signed Element must contain an ID. SAML Response rejected', 870 OneLogin_Saml2_ValidationError::ID_NOT_FOUND_IN_SIGNED_ELEMENT 871 ); 872 } 873 874 if (in_array($idValue, $verifiedIds)) { 875 throw new OneLogin_Saml2_ValidationError( 876 'Duplicated ID. SAML Response rejected', 877 OneLogin_Saml2_ValidationError::DUPLICATED_ID_IN_SIGNED_ELEMENTS 878 ); 879 } 880 $verifiedIds[] = $idValue; 881 882 $ref = $signNode->getElementsByTagName('Reference'); 883 if ($ref->length == 1) { 884 $ref = $ref->item(0); 885 $sei = $ref->getAttribute('URI'); 886 if (!empty($sei)) { 887 $sei = substr($sei, 1); 888 889 if ($sei != $idValue) { 890 throw new OneLogin_Saml2_ValidationError( 891 'Found an invalid Signed Element. SAML Response rejected', 892 OneLogin_Saml2_ValidationError::INVALID_SIGNED_ELEMENT 893 ); 894 } 895 896 if (in_array($sei, $verifiedSeis)) { 897 throw new OneLogin_Saml2_ValidationError( 898 'Duplicated Reference URI. SAML Response rejected', 899 OneLogin_Saml2_ValidationError::DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS 900 ); 901 } 902 $verifiedSeis[] = $sei; 903 } 904 } else { 905 throw new OneLogin_Saml2_ValidationError( 906 'Unexpected number of Reference nodes found for signature. SAML Response rejected.', 907 OneLogin_Saml2_ValidationError::UNEXPECTED_REFERENCE 908 ); 909 } 910 $signedElements[] = $signedElement; 911 } 912 913 // Check SignedElements 914 if (!empty($signedElements) && !$this->validateSignedElements($signedElements)) { 915 throw new OneLogin_Saml2_ValidationError( 916 'Found an unexpected Signature Element. SAML Response rejected', 917 OneLogin_Saml2_ValidationError::UNEXPECTED_SIGNED_ELEMENTS 918 ); 919 } 920 return $signedElements; 921 } 922 923 /** 924 * Verifies that the document is still valid according Conditions Element. 925 * 926 * @return bool 927 * 928 * @throws Exception 929 * @throws OneLogin_Saml2_ValidationError 930 */ 931 public function validateTimestamps() 932 { 933 if ($this->encrypted) { 934 $document = $this->decryptedDocument; 935 } else { 936 $document = $this->document; 937 } 938 939 $timestampNodes = $document->getElementsByTagName('Conditions'); 940 for ($i = 0; $i < $timestampNodes->length; $i++) { 941 $nbAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotBefore"); 942 $naAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotOnOrAfter"); 943 if ($nbAttribute && OneLogin_Saml2_Utils::parseSAML2Time($nbAttribute->textContent) > time() + OneLogin_Saml2_Constants::ALLOWED_CLOCK_DRIFT) { 944 throw new OneLogin_Saml2_ValidationError( 945 'Could not validate timestamp: not yet valid. Check system clock.', 946 OneLogin_Saml2_ValidationError::ASSERTION_TOO_EARLY 947 ); 948 } 949 if ($naAttribute && OneLogin_Saml2_Utils::parseSAML2Time($naAttribute->textContent) + OneLogin_Saml2_Constants::ALLOWED_CLOCK_DRIFT <= time()) { 950 throw new OneLogin_Saml2_ValidationError( 951 'Could not validate timestamp: expired. Check system clock.', 952 OneLogin_Saml2_ValidationError::ASSERTION_EXPIRED 953 ); 954 } 955 } 956 return true; 957 } 958 959 /** 960 * Verifies that the document has the expected signed nodes. 961 * 962 * @param $signedElements 963 * 964 * @return bool 965 * 966 * @throws OneLogin_Saml2_ValidationError 967 */ 968 public function validateSignedElements($signedElements) 969 { 970 if (count($signedElements) > 2) { 971 return false; 972 } 973 974 $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response'; 975 $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion'; 976 977 $ocurrence = array_count_values($signedElements); 978 if ((in_array($responseTag, $signedElements) && $ocurrence[$responseTag] > 1) || 979 (in_array($assertionTag, $signedElements) && $ocurrence[$assertionTag] > 1) || 980 !in_array($responseTag, $signedElements) && !in_array($assertionTag, $signedElements) 981 ) { 982 return false; 983 } 984 985 // Check that the signed elements found here, are the ones that will be verified 986 // by OneLogin_Saml2_Utils->validateSign() 987 if (in_array($responseTag, $signedElements)) { 988 $expectedSignatureNodes = OneLogin_Saml2_Utils::query($this->document, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH); 989 if ($expectedSignatureNodes->length != 1) { 990 throw new OneLogin_Saml2_ValidationError( 991 "Unexpected number of Response signatures found. SAML Response rejected.", 992 OneLogin_Saml2_ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE 993 ); 994 } 995 } 996 997 if (in_array($assertionTag, $signedElements)) { 998 $expectedSignatureNodes = $this->_query(OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH); 999 if ($expectedSignatureNodes->length != 1) { 1000 throw new OneLogin_Saml2_ValidationError( 1001 "Unexpected number of Assertion signatures found. SAML Response rejected.", 1002 OneLogin_Saml2_ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION 1003 ); 1004 } 1005 } 1006 1007 return true; 1008 } 1009 1010 /** 1011 * Extracts a node from the DOMDocument (Assertion). 1012 * 1013 * @param string $assertionXpath Xpath Expression 1014 * 1015 * @return DOMNodeList The queried node 1016 */ 1017 protected function _queryAssertion($assertionXpath) 1018 { 1019 if ($this->encrypted) { 1020 $xpath = new DOMXPath($this->decryptedDocument); 1021 } else { 1022 $xpath = new DOMXPath($this->document); 1023 } 1024 1025 $xpath->registerNamespace('samlp', OneLogin_Saml2_Constants::NS_SAMLP); 1026 $xpath->registerNamespace('saml', OneLogin_Saml2_Constants::NS_SAML); 1027 $xpath->registerNamespace('ds', OneLogin_Saml2_Constants::NS_DS); 1028 $xpath->registerNamespace('xenc', OneLogin_Saml2_Constants::NS_XENC); 1029 1030 $assertionNode = '/samlp:Response/saml:Assertion'; 1031 $signatureQuery = $assertionNode . '/ds:Signature/ds:SignedInfo/ds:Reference'; 1032 $assertionReferenceNode = $xpath->query($signatureQuery)->item(0); 1033 if (!$assertionReferenceNode) { 1034 // is the response signed as a whole? 1035 $signatureQuery = '/samlp:Response/ds:Signature/ds:SignedInfo/ds:Reference'; 1036 $responseReferenceNode = $xpath->query($signatureQuery)->item(0); 1037 if ($responseReferenceNode) { 1038 $uri = $responseReferenceNode->attributes->getNamedItem('URI')->nodeValue; 1039 if (empty($uri)) { 1040 $id = $responseReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue; 1041 } else { 1042 $id = substr($responseReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1); 1043 } 1044 $nameQuery = "/samlp:Response[@ID='$id']/saml:Assertion" . $assertionXpath; 1045 } else { 1046 $nameQuery = "/samlp:Response/saml:Assertion" . $assertionXpath; 1047 } 1048 } else { 1049 $uri = $assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue; 1050 if (empty($uri)) { 1051 $id = $assertionReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue; 1052 } else { 1053 $id = substr($assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1); 1054 } 1055 $nameQuery = $assertionNode."[@ID='$id']" . $assertionXpath; 1056 } 1057 1058 return $xpath->query($nameQuery); 1059 } 1060 1061 /** 1062 * Extracts nodes that match the query from the DOMDocument (Response Menssage) 1063 * 1064 * @param string $query Xpath Expresion 1065 * 1066 * @return DOMNodeList The queried nodes 1067 */ 1068 private function _query($query) 1069 { 1070 if ($this->encrypted) { 1071 return OneLogin_Saml2_Utils::query($this->decryptedDocument, $query); 1072 } else { 1073 return OneLogin_Saml2_Utils::query($this->document, $query); 1074 } 1075 } 1076 1077 /** 1078 * Decrypts the Assertion (DOMDocument) 1079 * 1080 * @param DomNode $dom DomDocument 1081 * 1082 * @return DOMDocument Decrypted Assertion 1083 * 1084 * @throws OneLogin_Saml2_Error 1085 * @throws OneLogin_Saml2_ValidationError 1086 */ 1087 protected function _decryptAssertion($dom) 1088 { 1089 $pem = $this->_settings->getSPkey(); 1090 if (empty($pem)) { 1091 throw new OneLogin_Saml2_Error( 1092 "No private key available, check settings", 1093 OneLogin_Saml2_Error::PRIVATE_KEY_NOT_FOUND 1094 ); 1095 } 1096 $objenc = new XMLSecEnc(); 1097 $encData = $objenc->locateEncryptedData($dom); 1098 if (!$encData) { 1099 throw new OneLogin_Saml2_ValidationError( 1100 "Cannot locate encrypted assertion", 1101 OneLogin_Saml2_ValidationError::MISSING_ENCRYPTED_ELEMENT 1102 ); 1103 } 1104 $objenc->setNode($encData); 1105 $objenc->type = $encData->getAttribute("Type"); 1106 if (!$objKey = $objenc->locateKey()) { 1107 throw new OneLogin_Saml2_ValidationError( 1108 "Unknown algorithm", 1109 OneLogin_Saml2_ValidationError::KEY_ALGORITHM_ERROR 1110 ); 1111 } 1112 $key = null; 1113 if ($objKeyInfo = $objenc->locateKeyInfo($objKey)) { 1114 if ($objKeyInfo->isEncrypted) { 1115 $objencKey = $objKeyInfo->encryptedCtx; 1116 $objKeyInfo->loadKey($pem, false, false); 1117 $key = $objencKey->decryptKey($objKeyInfo); 1118 } else { 1119 // symmetric encryption key support 1120 $objKeyInfo->loadKey($pem, false, false); 1121 } 1122 } 1123 1124 if (empty($objKey->key)) { 1125 $objKey->loadKey($key); 1126 } 1127 $decryptedXML = $objenc->decryptNode($objKey, false); 1128 $decrypted = new DOMDocument(); 1129 $check = OneLogin_Saml2_Utils::loadXML($decrypted, $decryptedXML); 1130 if ($check === false) { 1131 throw new Exception('Error: string from decrypted assertion could not be loaded into a XML document'); 1132 } 1133 if ($encData->parentNode instanceof DOMDocument) { 1134 return $decrypted; 1135 } else { 1136 $decrypted = $decrypted->documentElement; 1137 $encryptedAssertion = $encData->parentNode; 1138 $container = $encryptedAssertion->parentNode; 1139 1140 // Fix possible issue with saml namespace 1141 if (!$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml') 1142 && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2') 1143 && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns') 1144 && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml') 1145 && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2') 1146 ) { 1147 if (strpos($encryptedAssertion->tagName, 'saml2:') !== false) { 1148 $ns = 'xmlns:saml2'; 1149 } else if (strpos($encryptedAssertion->tagName, 'saml:') !== false) { 1150 $ns = 'xmlns:saml'; 1151 } else { 1152 $ns = 'xmlns'; 1153 } 1154 $decrypted->setAttributeNS('http://www.w3.org/2000/xmlns/', $ns, OneLogin_Saml2_Constants::NS_SAML); 1155 } 1156 1157 OneLogin_Saml2_Utils::treeCopyReplace($encryptedAssertion, $decrypted); 1158 1159 // Rebuild the DOM will fix issues with namespaces as well 1160 $dom = new DOMDocument(); 1161 return OneLogin_Saml2_Utils::loadXML($dom, $container->ownerDocument->saveXML()); 1162 } 1163 } 1164 1165 /** 1166 * After execute a validation process, if fails this method returns the cause 1167 * 1168 * @return string Cause 1169 */ 1170 public function getError() 1171 { 1172 return $this->_error; 1173 } 1174 1175 /** 1176 * Returns the SAML Response document (If contains an encrypted assertion, decrypts it) 1177 * 1178 * @return DomDocument SAML Response 1179 */ 1180 public function getXMLDocument() 1181 { 1182 if ($this->encrypted) { 1183 return $this->decryptedDocument; 1184 } else { 1185 return $this->document; 1186 } 1187 } 1188} 1189