1<?php 2 3declare(strict_types=1); 4 5namespace SAML2; 6 7use DOMElement; 8use DOMNode; 9use DOMNodeList; 10use RobRichards\XMLSecLibs\XMLSecEnc; 11use RobRichards\XMLSecLibs\XMLSecurityKey; 12use Webmozart\Assert\Assert; 13 14use SAML2\Exception\RuntimeException; 15use SAML2\Utilities\Temporal; 16use SAML2\XML\Chunk; 17use SAML2\XML\saml\Issuer; 18use SAML2\XML\saml\NameID; 19use SAML2\XML\saml\SubjectConfirmation; 20 21/** 22 * Class representing a SAML 2 assertion. 23 * 24 * @package SimpleSAMLphp 25 */ 26class Assertion extends SignedElement 27{ 28 /** 29 * The identifier of this assertion. 30 * 31 * @var string 32 */ 33 private $id; 34 35 /** 36 * The issue timestamp of this assertion, as an UNIX timestamp. 37 * 38 * @var int 39 */ 40 private $issueInstant; 41 42 /** 43 * The issuer of this assertion. 44 * 45 * If the issuer's format is \SAML2\Constants::NAMEID_ENTITY, this property will just take the issuer's string 46 * value. 47 * 48 * @var \SAML2\XML\saml\Issuer 49 */ 50 private $issuer; 51 52 /** 53 * The NameId of the subject in the assertion. 54 * 55 * If the NameId is null, no subject was included in the assertion. 56 * 57 * @var \SAML2\XML\saml\NameID|null 58 */ 59 private $nameId; 60 61 /** 62 * The encrypted NameId of the subject. 63 * 64 * If this is not null, the NameId needs decryption before it can be accessed. 65 * 66 * @var \DOMElement|null 67 */ 68 private $encryptedNameId; 69 70 /** 71 * The encrypted Attributes. 72 * 73 * If this is not an empty array, these Attributes need decryption before they can be used. 74 * 75 * @var \DOMElement[] 76 */ 77 private $encryptedAttributes; 78 79 /** 80 * Private key we should use to encrypt the attributes. 81 * 82 * @var XMLSecurityKey|null 83 */ 84 private $encryptionKey; 85 86 /** 87 * The earliest time this assertion is valid, as an UNIX timestamp. 88 * 89 * @var int|null 90 */ 91 private $notBefore; 92 93 /** 94 * The time this assertion expires, as an UNIX timestamp. 95 * 96 * @var int|null 97 */ 98 private $notOnOrAfter; 99 100 /** 101 * The set of audiences that are allowed to receive this assertion. 102 * 103 * This is an array of valid service providers. 104 * 105 * If no restrictions on the audience are present, this variable contains null. 106 * 107 * @var array|null 108 */ 109 private $validAudiences; 110 111 /** 112 * The session expiration timestamp. 113 * 114 * @var int|null 115 */ 116 private $sessionNotOnOrAfter = null; 117 118 /** 119 * The session index for this user on the IdP. 120 * 121 * Contains null if no session index is present. 122 * 123 * @var string|null 124 */ 125 private $sessionIndex = null; 126 127 /** 128 * The timestamp the user was authenticated, as an UNIX timestamp. 129 * 130 * @var int|null 131 */ 132 private $authnInstant = null; 133 134 /** 135 * The authentication context reference for this assertion. 136 * 137 * @var string|null 138 */ 139 private $authnContextClassRef = null; 140 141 /** 142 * Authentication context declaration provided by value. 143 * 144 * See: 145 * @url http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf 146 * 147 * @var \SAML2\XML\Chunk|null 148 */ 149 private $authnContextDecl = null; 150 151 /** 152 * URI reference that identifies an authentication context declaration. 153 * 154 * The URI reference MAY directly resolve into an XML document containing the referenced declaration. 155 * 156 * @var string|null 157 */ 158 private $authnContextDeclRef = null; 159 160 /** 161 * The list of AuthenticatingAuthorities for this assertion. 162 * 163 * @var array 164 */ 165 private $AuthenticatingAuthority = []; 166 167 /** 168 * The attributes, as an associative array, indexed by attribute name 169 * 170 * To ease handling, all attribute values are represented as an array of values, also for values with a multiplicity 171 * of single. There are 5 possible variants of datatypes for the values: a string, an integer, an array, a 172 * DOMNodeList or a SAML2\XML\saml\NameID object. 173 * 174 * If the attribute is an eduPersonTargetedID, the values will be SAML2\XML\saml\NameID objects. 175 * If the attribute value has an type-definition (xsi:string or xsi:int), the values will be of that type. 176 * If the attribute value contains a nested XML structure, the values will be a DOMNodeList 177 * In all other cases the values are treated as strings 178 * 179 * **WARNING** a DOMNodeList cannot be serialized without data-loss and should be handled explicitly 180 * 181 * @var array multi-dimensional array of \DOMNodeList|\SAML2\XML\saml\NameID|string|int|array 182 */ 183 private $attributes = []; 184 185 /** 186 * The attributes values types as per http://www.w3.org/2001/XMLSchema definitions 187 * the variable is as an associative array, indexed by attribute name 188 * 189 * when parsing assertion, the variable will be: 190 * - <attribute name> => [<Value1's xs type>|null, <xs type Value2>|null, ...] 191 * array will always have the same size of the array of vaules in $attributes for the same <attribute name> 192 * 193 * when generating assertion, the varuable can be: 194 * - null : backward compatibility 195 * - <attribute name> => <xs type> : all values for the given attribute will have the same xs type 196 * - <attribute name> => [<Value1's xs type>|null, <xs type Value2>|null, ...] : Nth value will have type of the 197 * Nth in the array 198 * 199 * @var array multi-dimensional array of array 200 */ 201 private $attributesValueTypes = []; 202 203 /** 204 * The NameFormat used on all attributes. 205 * 206 * If more than one NameFormat is used, this will contain the unspecified nameformat. 207 * 208 * @var string 209 */ 210 private $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED; 211 212 /** 213 * The data needed to verify the signature. 214 * 215 * @var array|null 216 */ 217 private $signatureData = null; 218 219 /** 220 * Boolean that indicates if attributes are encrypted in the assertion or not. 221 * 222 * @var boolean 223 */ 224 private $requiredEncAttributes = false; 225 226 /** 227 * The SubjectConfirmation elements of the Subject in the assertion. 228 * 229 * @var \SAML2\XML\saml\SubjectConfirmation[] 230 */ 231 private $SubjectConfirmation = []; 232 233 /** 234 * @var bool 235 */ 236 protected $wasSignedAtConstruction = false; 237 238 /** 239 * @var string|null 240 */ 241 private $signatureMethod; 242 243 244 /** 245 * Constructor for SAML 2 assertions. 246 * 247 * @param \DOMElement|null $xml The input assertion. 248 * @throws \Exception 249 */ 250 public function __construct(DOMElement $xml = null) 251 { 252 // Create an Issuer 253 $issuer = new Issuer(); 254 $issuer->setValue(''); 255 256 $this->id = Utils::getContainer()->generateId(); 257 $this->issueInstant = Temporal::getTime(); 258 $this->issuer = $issuer; 259 $this->authnInstant = Temporal::getTime(); 260 261 if ($xml === null) { 262 return; 263 } 264 265 if (!$xml->hasAttribute('ID')) { 266 throw new \Exception('Missing ID attribute on SAML assertion.'); 267 } 268 $this->id = $xml->getAttribute('ID'); 269 270 if ($xml->getAttribute('Version') !== '2.0') { 271 /* Currently a very strict check. */ 272 throw new \Exception('Unsupported version: '.$xml->getAttribute('Version')); 273 } 274 275 $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant')); 276 277 /** @var \DOMElement[] $issuer */ 278 $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer'); 279 if (empty($issuer)) { 280 throw new \Exception('Missing <saml:Issuer> in assertion.'); 281 } 282 283 $this->issuer = new Issuer($issuer[0]); 284 285 $this->parseSubject($xml); 286 $this->parseConditions($xml); 287 $this->parseAuthnStatement($xml); 288 $this->parseAttributes($xml); 289 $this->parseEncryptedAttributes($xml); 290 $this->parseSignature($xml); 291 } 292 293 294 /** 295 * Parse subject in assertion. 296 * 297 * @param \DOMElement $xml The assertion XML element. 298 * @throws \Exception 299 * @return void 300 */ 301 private function parseSubject(DOMElement $xml) : void 302 { 303 /** @var \DOMElement[] $subject */ 304 $subject = Utils::xpQuery($xml, './saml_assertion:Subject'); 305 if (empty($subject)) { 306 /* No Subject node. */ 307 308 return; 309 } elseif (count($subject) > 1) { 310 throw new \Exception('More than one <saml:Subject> in <saml:Assertion>.'); 311 } 312 $subject = $subject[0]; 313 314 /** @var \DOMElement[] $nameId */ 315 $nameId = Utils::xpQuery( 316 $subject, 317 './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData' 318 ); 319 if (count($nameId) > 1) { 320 throw new \Exception('More than one <saml:NameID> or <saml:EncryptedID> in <saml:Subject>.'); 321 } elseif (!empty($nameId)) { 322 $nameId = $nameId[0]; 323 if ($nameId->localName === 'EncryptedData') { 324 /* The NameID element is encrypted. */ 325 $this->encryptedNameId = $nameId; 326 } else { 327 $this->nameId = new NameID($nameId); 328 } 329 } 330 331 /** @var \DOMElement[] $subjectConfirmation */ 332 $subjectConfirmation = Utils::xpQuery($subject, './saml_assertion:SubjectConfirmation'); 333 if (empty($subjectConfirmation) && empty($nameId)) { 334 throw new \Exception('Missing <saml:SubjectConfirmation> in <saml:Subject>.'); 335 } 336 337 foreach ($subjectConfirmation as $sc) { 338 $this->SubjectConfirmation[] = new SubjectConfirmation($sc); 339 } 340 } 341 342 343 /** 344 * Parse conditions in assertion. 345 * 346 * @param \DOMElement $xml The assertion XML element. 347 * @throws \Exception 348 * @return void 349 */ 350 private function parseConditions(DOMElement $xml) : void 351 { 352 /** @var \DOMElement[] $conditions */ 353 $conditions = Utils::xpQuery($xml, './saml_assertion:Conditions'); 354 if (empty($conditions)) { 355 /* No <saml:Conditions> node. */ 356 357 return; 358 } elseif (count($conditions) > 1) { 359 throw new \Exception('More than one <saml:Conditions> in <saml:Assertion>.'); 360 } 361 $conditions = $conditions[0]; 362 363 if ($conditions->hasAttribute('NotBefore')) { 364 $notBefore = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotBefore')); 365 if ($this->getNotBefore() === null || $this->getNotBefore() < $notBefore) { 366 $this->setNotBefore($notBefore); 367 } 368 } 369 if ($conditions->hasAttribute('NotOnOrAfter')) { 370 $notOnOrAfter = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotOnOrAfter')); 371 if ($this->getNotOnOrAfter() === null || $this->getNotOnOrAfter() > $notOnOrAfter) { 372 $this->setNotOnOrAfter($notOnOrAfter); 373 } 374 } 375 376 foreach ($conditions->childNodes as $node) { 377 if (!$node instanceof DOMElement) { 378 continue; 379 } 380 if ($node->namespaceURI !== Constants::NS_SAML) { 381 throw new \Exception('Unknown namespace of condition: '.var_export($node->namespaceURI, true)); 382 } 383 switch ($node->localName) { 384 case 'AudienceRestriction': 385 $audiences = Utils::extractStrings($node, Constants::NS_SAML, 'Audience'); 386 if ($this->validAudiences === null) { 387 /* The first (and probably last) AudienceRestriction element. */ 388 $this->validAudiences = $audiences; 389 } else { 390 /* 391 * The set of AudienceRestriction are ANDed together, so we need 392 * the subset that are present in all of them. 393 */ 394 $this->validAudiences = array_intersect($this->validAudiences, $audiences); 395 } 396 break; 397 case 'OneTimeUse': 398 /* Currently ignored. */ 399 break; 400 case 'ProxyRestriction': 401 /* Currently ignored. */ 402 break; 403 default: 404 throw new \Exception('Unknown condition: '.var_export($node->localName, true)); 405 } 406 } 407 } 408 409 410 /** 411 * Parse AuthnStatement in assertion. 412 * 413 * @param \DOMElement $xml The assertion XML element. 414 * @throws \Exception 415 * @return void 416 */ 417 private function parseAuthnStatement(DOMElement $xml) : void 418 { 419 /** @var \DOMElement[] $authnStatements */ 420 $authnStatements = Utils::xpQuery($xml, './saml_assertion:AuthnStatement'); 421 if (empty($authnStatements)) { 422 $this->authnInstant = null; 423 424 return; 425 } elseif (count($authnStatements) > 1) { 426 throw new \Exception('More than one <saml:AuthnStatement> in <saml:Assertion> not supported.'); 427 } 428 $authnStatement = $authnStatements[0]; 429 430 if (!$authnStatement->hasAttribute('AuthnInstant')) { 431 throw new \Exception('Missing required AuthnInstant attribute on <saml:AuthnStatement>.'); 432 } 433 $this->authnInstant = Utils::xsDateTimeToTimestamp($authnStatement->getAttribute('AuthnInstant')); 434 435 if ($authnStatement->hasAttribute('SessionNotOnOrAfter')) { 436 $this->sessionNotOnOrAfter = Utils::xsDateTimeToTimestamp( 437 $authnStatement->getAttribute('SessionNotOnOrAfter') 438 ); 439 } 440 441 if ($authnStatement->hasAttribute('SessionIndex')) { 442 $this->sessionIndex = $authnStatement->getAttribute('SessionIndex'); 443 } 444 445 $this->parseAuthnContext($authnStatement); 446 } 447 448 449 /** 450 * Parse AuthnContext in AuthnStatement. 451 * 452 * @param \DOMElement $authnStatementEl 453 * @throws \Exception 454 * @return void 455 */ 456 private function parseAuthnContext(DOMElement $authnStatementEl) : void 457 { 458 // Get the AuthnContext element 459 /** @var \DOMElement[] $authnContexts */ 460 $authnContexts = Utils::xpQuery($authnStatementEl, './saml_assertion:AuthnContext'); 461 if (count($authnContexts) > 1) { 462 throw new \Exception('More than one <saml:AuthnContext> in <saml:AuthnStatement>.'); 463 } elseif (empty($authnContexts)) { 464 throw new \Exception('Missing required <saml:AuthnContext> in <saml:AuthnStatement>.'); 465 } 466 $authnContextEl = $authnContexts[0]; 467 468 // Get the AuthnContextDeclRef (if available) 469 /** @var \DOMElement[] $authnContextDeclRefs */ 470 $authnContextDeclRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDeclRef'); 471 if (count($authnContextDeclRefs) > 1) { 472 throw new \Exception( 473 'More than one <saml:AuthnContextDeclRef> found?' 474 ); 475 } elseif (count($authnContextDeclRefs) === 1) { 476 $this->setAuthnContextDeclRef(trim($authnContextDeclRefs[0]->textContent)); 477 } 478 479 // Get the AuthnContextDecl (if available) 480 /** @var \DOMElement[] $authnContextDecls */ 481 $authnContextDecls = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDecl'); 482 if (count($authnContextDecls) > 1) { 483 throw new \Exception( 484 'More than one <saml:AuthnContextDecl> found?' 485 ); 486 } elseif (count($authnContextDecls) === 1) { 487 $this->setAuthnContextDecl(new Chunk($authnContextDecls[0])); 488 } 489 490 // Get the AuthnContextClassRef (if available) 491 /** @var \DOMElement[] $authnContextClassRefs */ 492 $authnContextClassRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextClassRef'); 493 if (count($authnContextClassRefs) > 1) { 494 throw new \Exception('More than one <saml:AuthnContextClassRef> in <saml:AuthnContext>.'); 495 } elseif (count($authnContextClassRefs) === 1) { 496 $this->setAuthnContextClassRef(trim($authnContextClassRefs[0]->textContent)); 497 } 498 499 // Constraint from XSD: MUST have one of the three 500 if (empty($this->authnContextClassRef) && empty($this->authnContextDecl) && empty($this->authnContextDeclRef)) { 501 throw new \Exception( 502 'Missing either <saml:AuthnContextClassRef> or <saml:AuthnContextDeclRef> or <saml:AuthnContextDecl>' 503 ); 504 } 505 506 $this->AuthenticatingAuthority = Utils::extractStrings( 507 $authnContextEl, 508 Constants::NS_SAML, 509 'AuthenticatingAuthority' 510 ); 511 } 512 513 514 /** 515 * Parse attribute statements in assertion. 516 * 517 * @param \DOMElement $xml The XML element with the assertion. 518 * @throws \Exception 519 * @return void 520 */ 521 private function parseAttributes(DOMElement $xml) : void 522 { 523 $firstAttribute = true; 524 /** @var \DOMElement[] $attributes */ 525 $attributes = Utils::xpQuery($xml, './saml_assertion:AttributeStatement/saml_assertion:Attribute'); 526 foreach ($attributes as $attribute) { 527 if (!$attribute->hasAttribute('Name')) { 528 throw new \Exception('Missing name on <saml:Attribute> element.'); 529 } 530 $name = $attribute->getAttribute('Name'); 531 532 if ($attribute->hasAttribute('NameFormat')) { 533 $nameFormat = $attribute->getAttribute('NameFormat'); 534 } else { 535 $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED; 536 } 537 538 if ($firstAttribute) { 539 $this->nameFormat = $nameFormat; 540 $firstAttribute = false; 541 } else { 542 if ($this->nameFormat !== $nameFormat) { 543 $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED; 544 } 545 } 546 547 if (!array_key_exists($name, $this->attributes)) { 548 $this->attributes[$name] = []; 549 $this->attributesValueTypes[$name] = []; 550 } 551 552 $this->parseAttributeValue($attribute, $name); 553 } 554 } 555 556 557 /** 558 * @param \DOMNode $attribute 559 * @param string $attributeName 560 * @return void 561 */ 562 private function parseAttributeValue(DOMNode $attribute, string $attributeName) : void 563 { 564 /** @var \DOMElement[] $values */ 565 $values = Utils::xpQuery($attribute, './saml_assertion:AttributeValue'); 566 567 if ($attributeName === Constants::EPTI_URN_MACE || $attributeName === Constants::EPTI_URN_OID) { 568 foreach ($values as $index => $eptiAttributeValue) { 569 /** @var \DOMElement[] $eptiNameId */ 570 $eptiNameId = Utils::xpQuery($eptiAttributeValue, './saml_assertion:NameID'); 571 572 if (count($eptiNameId) === 1) { 573 $this->attributes[$attributeName][] = new NameID($eptiNameId[0]); 574 } else { 575 /* Fall back for legacy IdPs sending string value (e.g. SSP < 1.15) */ 576 Utils::getContainer()->getLogger()->warning( 577 sprintf("Attribute %s (EPTI) value %d is not an XML NameId", $attributeName, $index) 578 ); 579 $nameId = new NameID(); 580 $nameId->setValue($eptiAttributeValue->textContent); 581 $this->attributes[$attributeName][] = $nameId; 582 } 583 } 584 585 return; 586 } 587 588 foreach ($values as $value) { 589 $hasNonTextChildElements = false; 590 foreach ($value->childNodes as $childNode) { 591 /** @var \DOMNode $childNode */ 592 if ($childNode->nodeType !== XML_TEXT_NODE) { 593 $hasNonTextChildElements = true; 594 break; 595 } 596 } 597 598 $type = $value->getAttribute('xsi:type'); 599 if ($type === '') { 600 $type = null; 601 } 602 $this->attributesValueTypes[$attributeName][] = $type; 603 604 if ($hasNonTextChildElements) { 605 $this->attributes[$attributeName][] = $value->childNodes; 606 continue; 607 } 608 609 if ($type === 'xs:integer') { 610 $this->attributes[$attributeName][] = (int) $value->textContent; 611 } else { 612 $this->attributes[$attributeName][] = trim($value->textContent); 613 } 614 } 615 } 616 617 618 /** 619 * Parse encrypted attribute statements in assertion. 620 * 621 * @param \DOMElement $xml The XML element with the assertion. 622 * @return void 623 */ 624 private function parseEncryptedAttributes(DOMElement $xml) : void 625 { 626 /** @var \DOMElement[] encryptedAttributes */ 627 $this->encryptedAttributes = Utils::xpQuery( 628 $xml, 629 './saml_assertion:AttributeStatement/saml_assertion:EncryptedAttribute' 630 ); 631 } 632 633 634 /** 635 * Parse signature on assertion. 636 * 637 * @param \DOMElement $xml The assertion XML element. 638 * @return void 639 */ 640 private function parseSignature(DOMElement $xml) : void 641 { 642 /** @var \DOMAttr[] $signatureMethod */ 643 $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm'); 644 645 /* Validate the signature element of the message. */ 646 $sig = Utils::validateElement($xml); 647 if ($sig !== false) { 648 $this->wasSignedAtConstruction = true; 649 $this->setCertificates($sig['Certificates']); 650 $this->setSignatureData($sig); 651 $this->setSignatureMethod($signatureMethod[0]->value); 652 } 653 } 654 655 656 /** 657 * Validate this assertion against a public key. 658 * 659 * If no signature was present on the assertion, we will return false. 660 * Otherwise, true will be returned. An exception is thrown if the 661 * signature validation fails. 662 * 663 * @param XMLSecurityKey $key The key we should check against. 664 * @return boolean true if successful, false if it is unsigned. 665 */ 666 public function validate(XMLSecurityKey $key) : bool 667 { 668 Assert::same($key->type, XMLSecurityKey::RSA_SHA256); 669 670 if ($this->signatureData === null) { 671 return false; 672 } 673 674 Utils::validateSignature($this->signatureData, $key); 675 676 return true; 677 } 678 679 680 /** 681 * Retrieve the identifier of this assertion. 682 * 683 * @return string The identifier of this assertion. 684 */ 685 public function getId() : string 686 { 687 return $this->id; 688 } 689 690 691 /** 692 * Set the identifier of this assertion. 693 * 694 * @param string $id The new identifier of this assertion. 695 * @return void 696 */ 697 public function setId(string $id) : void 698 { 699 $this->id = $id; 700 } 701 702 703 /** 704 * Retrieve the issue timestamp of this assertion. 705 * 706 * @return int The issue timestamp of this assertion, as an UNIX timestamp. 707 */ 708 public function getIssueInstant() : int 709 { 710 return $this->issueInstant; 711 } 712 713 714 /** 715 * Set the issue timestamp of this assertion. 716 * 717 * @param int $issueInstant The new issue timestamp of this assertion, as an UNIX timestamp. 718 * @return void 719 */ 720 public function setIssueInstant(int $issueInstant) : void 721 { 722 $this->issueInstant = $issueInstant; 723 } 724 725 726 /** 727 * Retrieve the issuer if this assertion. 728 * 729 * @return \SAML2\XML\saml\Issuer The issuer of this assertion. 730 */ 731 public function getIssuer() : Issuer 732 { 733 return $this->issuer; 734 } 735 736 737 /** 738 * Set the issuer of this message. 739 * 740 * @param \SAML2\XML\saml\Issuer $issuer The new issuer of this assertion. 741 * @return void 742 */ 743 public function setIssuer(Issuer $issuer) : void 744 { 745 $this->issuer = $issuer; 746 } 747 748 749 /** 750 * Retrieve the NameId of the subject in the assertion. 751 * 752 * @throws \Exception 753 * @return \SAML2\XML\saml\NameID|null The name identifier of the assertion. 754 */ 755 public function getNameId() : ?NameID 756 { 757 if ($this->encryptedNameId !== null) { 758 throw new \Exception('Attempted to retrieve encrypted NameID without decrypting it first.'); 759 } 760 761 return $this->nameId; 762 } 763 764 765 /** 766 * Set the NameId of the subject in the assertion. 767 * 768 * The NameId must be a \SAML2\XML\saml\NameID object. 769 * 770 * @see \SAML2\Utils::addNameId() 771 * @param \SAML2\XML\saml\NameID|null $nameId The name identifier of the assertion. 772 * @return void 773 */ 774 public function setNameId(NameID $nameId = null) : void 775 { 776 $this->nameId = $nameId; 777 } 778 779 780 /** 781 * Check whether the NameId is encrypted. 782 * 783 * @return bool True if the NameId is encrypted, false if not. 784 */ 785 public function isNameIdEncrypted() : bool 786 { 787 return $this->encryptedNameId !== null; 788 } 789 790 791 /** 792 * Encrypt the NameID in the Assertion. 793 * 794 * @param XMLSecurityKey $key The encryption key. 795 * @return void 796 */ 797 public function encryptNameId(XMLSecurityKey $key) : void 798 { 799 if ($this->nameId === null) { 800 throw new \Exception('Cannot encrypt NameID, no NameID set.'); 801 } 802 /* First create an XML representation of the NameID. */ 803 $doc = DOMDocumentFactory::create(); 804 $root = $doc->createElement('root'); 805 $doc->appendChild($root); 806 $this->nameId->toXML($root); 807 /** @var \DOMElement $nameId */ 808 $nameId = $root->firstChild; 809 810 Utils::getContainer()->debugMessage($nameId, 'encrypt'); 811 812 /* Encrypt the NameID. */ 813 $enc = new XMLSecEnc(); 814 $enc->setNode($nameId); 815 // @codingStandardsIgnoreStart 816 $enc->type = XMLSecEnc::Element; 817 // @codingStandardsIgnoreEnd 818 819 $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); 820 $symmetricKey->generateSessionKey(); 821 $enc->encryptKey($key, $symmetricKey); 822 823 /** 824 * @var \DOMElement encryptedNameId 825 * @psalm-suppress UndefinedClass 826 */ 827 $this->encryptedNameId = $enc->encryptNode($symmetricKey); 828 $this->nameId = null; 829 } 830 831 832 /** 833 * Decrypt the NameId of the subject in the assertion. 834 * 835 * @param XMLSecurityKey $key The decryption key. 836 * @param array $blacklist Blacklisted decryption algorithms. 837 * @return void 838 */ 839 public function decryptNameId(XMLSecurityKey $key, array $blacklist = []) : void 840 { 841 if ($this->encryptedNameId === null) { 842 /* No NameID to decrypt. */ 843 844 return; 845 } 846 847 $nameId = Utils::decryptElement($this->encryptedNameId, $key, $blacklist); 848 Utils::getContainer()->debugMessage($nameId, 'decrypt'); 849 $this->nameId = new NameID($nameId); 850 851 $this->encryptedNameId = null; 852 } 853 854 855 /** 856 * Did this Assertion contain encrypted Attributes? 857 * 858 * @return bool 859 */ 860 public function hasEncryptedAttributes() : bool 861 { 862 return $this->encryptedAttributes !== []; 863 } 864 865 866 /** 867 * Decrypt the assertion attributes. 868 * 869 * @param XMLSecurityKey $key 870 * @param array $blacklist 871 * @throws \Exception 872 * @return void 873 */ 874 public function decryptAttributes(XMLSecurityKey $key, array $blacklist = []) : void 875 { 876 if (!$this->hasEncryptedAttributes()) { 877 return; 878 } 879 $firstAttribute = true; 880 $attributes = $this->getEncryptedAttributes(); 881 foreach ($attributes as $attributeEnc) { 882 /* Decrypt node <EncryptedAttribute> */ 883 $attribute = Utils::decryptElement( 884 $attributeEnc->getElementsByTagName('EncryptedData')->item(0), 885 $key, 886 $blacklist 887 ); 888 889 if (!$attribute->hasAttribute('Name')) { 890 throw new \Exception('Missing name on <saml:Attribute> element.'); 891 } 892 $name = $attribute->getAttribute('Name'); 893 894 if ($attribute->hasAttribute('NameFormat')) { 895 $nameFormat = $attribute->getAttribute('NameFormat'); 896 } else { 897 $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED; 898 } 899 900 if ($firstAttribute) { 901 $this->nameFormat = $nameFormat; 902 $firstAttribute = false; 903 } else { 904 if ($this->nameFormat !== $nameFormat) { 905 $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED; 906 } 907 } 908 909 if (!array_key_exists($name, $this->attributes)) { 910 $this->attributes[$name] = []; 911 } 912 913 $this->parseAttributeValue($attribute, $name); 914 } 915 } 916 917 918 /** 919 * Retrieve the earliest timestamp this assertion is valid. 920 * 921 * This function returns null if there are no restrictions on how early the 922 * assertion can be used. 923 * 924 * @return int|null The earliest timestamp this assertion is valid. 925 */ 926 public function getNotBefore() : ?int 927 { 928 return $this->notBefore; 929 } 930 931 932 /** 933 * Set the earliest timestamp this assertion can be used. 934 * 935 * Set this to null if no limit is required. 936 * 937 * @param int|null $notBefore The earliest timestamp this assertion is valid. 938 * @return void 939 */ 940 public function setNotBefore(int $notBefore = null) : void 941 { 942 $this->notBefore = $notBefore; 943 } 944 945 946 /** 947 * Retrieve the expiration timestamp of this assertion. 948 * 949 * This function returns null if there are no restrictions on how 950 * late the assertion can be used. 951 * 952 * @return int|null The latest timestamp this assertion is valid. 953 */ 954 public function getNotOnOrAfter() : ?int 955 { 956 return $this->notOnOrAfter; 957 } 958 959 960 /** 961 * Set the expiration timestamp of this assertion. 962 * 963 * Set this to null if no limit is required. 964 * 965 * @param int|null $notOnOrAfter The latest timestamp this assertion is valid. 966 * @return void 967 */ 968 public function setNotOnOrAfter(int $notOnOrAfter = null) : void 969 { 970 $this->notOnOrAfter = $notOnOrAfter; 971 } 972 973 974 /** 975 * Retrieve $requiredEncAttributes if attributes will be send encrypted 976 * 977 * @return bool True to encrypt attributes in the assertion. 978 */ 979 public function getRequiredEncAttributes() : bool 980 { 981 return $this->requiredEncAttributes; 982 } 983 984 985 /** 986 * Set $requiredEncAttributes if attributes will be send encrypted 987 * 988 * @param bool $ea true to encrypt attributes in the assertion. 989 * @return void 990 */ 991 public function setRequiredEncAttributes(bool $ea) : void 992 { 993 $this->requiredEncAttributes = $ea; 994 } 995 996 997 /** 998 * Retrieve the audiences that are allowed to receive this assertion. 999 * 1000 * This may be null, in which case all audiences are allowed. 1001 * 1002 * @return array|null The allowed audiences. 1003 */ 1004 public function getValidAudiences() : ?array 1005 { 1006 return $this->validAudiences; 1007 } 1008 1009 1010 /** 1011 * Set the audiences that are allowed to receive this assertion. 1012 * 1013 * This may be null, in which case all audiences are allowed. 1014 * 1015 * @param array|null $validAudiences The allowed audiences. 1016 * @return void 1017 */ 1018 public function setValidAudiences(array $validAudiences = null) : void 1019 { 1020 $this->validAudiences = $validAudiences; 1021 } 1022 1023 1024 /** 1025 * Retrieve the AuthnInstant of the assertion. 1026 * 1027 * @return int|null The timestamp the user was authenticated, or NULL if the user isn't authenticated. 1028 */ 1029 public function getAuthnInstant() : ?int 1030 { 1031 return $this->authnInstant; 1032 } 1033 1034 1035 /** 1036 * Set the AuthnInstant of the assertion. 1037 * 1038 * @param int|null $authnInstant Timestamp the user was authenticated, or NULL if we don't want an AuthnStatement. 1039 * @return void 1040 */ 1041 public function setAuthnInstant(int $authnInstant = null) : void 1042 { 1043 $this->authnInstant = $authnInstant; 1044 } 1045 1046 1047 /** 1048 * Retrieve the session expiration timestamp. 1049 * 1050 * This function returns null if there are no restrictions on the 1051 * session lifetime. 1052 * 1053 * @return int|null The latest timestamp this session is valid. 1054 */ 1055 public function getSessionNotOnOrAfter() : ?int 1056 { 1057 return $this->sessionNotOnOrAfter; 1058 } 1059 1060 1061 /** 1062 * Set the session expiration timestamp. 1063 * 1064 * Set this to null if no limit is required. 1065 * 1066 * @param int|null $sessionNotOnOrAfter The latest timestamp this session is valid. 1067 * @return void 1068 */ 1069 public function setSessionNotOnOrAfter(int $sessionNotOnOrAfter = null) : void 1070 { 1071 $this->sessionNotOnOrAfter = $sessionNotOnOrAfter; 1072 } 1073 1074 1075 /** 1076 * Retrieve the session index of the user at the IdP. 1077 * 1078 * @return string|null The session index of the user at the IdP. 1079 */ 1080 public function getSessionIndex() : ?string 1081 { 1082 return $this->sessionIndex; 1083 } 1084 1085 1086 /** 1087 * Set the session index of the user at the IdP. 1088 * 1089 * Note that the authentication context must be set before the 1090 * session index can be inluded in the assertion. 1091 * 1092 * @param string|null $sessionIndex The session index of the user at the IdP. 1093 * @return void 1094 */ 1095 public function setSessionIndex(string $sessionIndex = null) : void 1096 { 1097 $this->sessionIndex = $sessionIndex; 1098 } 1099 1100 1101 /** 1102 * Retrieve the authentication method used to authenticate the user. 1103 * 1104 * This will return null if no authentication statement was 1105 * included in the assertion. 1106 * 1107 * @return string|null The authentication method. 1108 */ 1109 public function getAuthnContextClassRef() : ?string 1110 { 1111 return $this->authnContextClassRef; 1112 } 1113 1114 1115 /** 1116 * Set the authentication method used to authenticate the user. 1117 * 1118 * If this is set to null, no authentication statement will be 1119 * included in the assertion. The default is null. 1120 * 1121 * @param string|null $authnContextClassRef The authentication method. 1122 * @return void 1123 */ 1124 public function setAuthnContextClassRef(string $authnContextClassRef = null) : void 1125 { 1126 $this->authnContextClassRef = $authnContextClassRef; 1127 } 1128 1129 1130 /** 1131 * Retrieve the signature method. 1132 * 1133 * @return string|null The signature method. 1134 */ 1135 public function getSignatureMethod() : ?string 1136 { 1137 return $this->signatureMethod; 1138 } 1139 1140 1141 /** 1142 * Set the signature method used. 1143 * 1144 * @param string|null $signatureMethod 1145 * @return void 1146 */ 1147 public function setSignatureMethod(string $signatureMethod = null) : void 1148 { 1149 $this->signatureMethod = $signatureMethod; 1150 } 1151 1152 1153 /** 1154 * Set the authentication context declaration. 1155 * 1156 * @param \SAML2\XML\Chunk $authnContextDecl 1157 * @throws \Exception 1158 * @return void 1159 */ 1160 public function setAuthnContextDecl(Chunk $authnContextDecl) : void 1161 { 1162 if (!empty($this->authnContextDeclRef)) { 1163 throw new \Exception( 1164 'AuthnContextDeclRef is already registered! May only have either a Decl or a DeclRef, not both!' 1165 ); 1166 } 1167 1168 $this->authnContextDecl = $authnContextDecl; 1169 } 1170 1171 1172 /** 1173 * Get the authentication context declaration. 1174 * 1175 * See: 1176 * @url http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf 1177 * 1178 * @return \SAML2\XML\Chunk|null 1179 */ 1180 public function getAuthnContextDecl() : ?Chunk 1181 { 1182 return $this->authnContextDecl; 1183 } 1184 1185 1186 /** 1187 * Set the authentication context declaration reference. 1188 * 1189 * @param string|null $authnContextDeclRef 1190 * @throws \Exception 1191 * @return void 1192 */ 1193 public function setAuthnContextDeclRef(string $authnContextDeclRef = null) : void 1194 { 1195 if (!empty($this->authnContextDecl)) { 1196 throw new \Exception( 1197 'AuthnContextDecl is already registered! May only have either a Decl or a DeclRef, not both!' 1198 ); 1199 } 1200 1201 $this->authnContextDeclRef = $authnContextDeclRef; 1202 } 1203 1204 1205 /** 1206 * Get the authentication context declaration reference. 1207 * URI reference that identifies an authentication context declaration. 1208 * 1209 * The URI reference MAY directly resolve into an XML document containing the referenced declaration. 1210 * 1211 * @return string|null 1212 */ 1213 public function getAuthnContextDeclRef() : ?string 1214 { 1215 return $this->authnContextDeclRef; 1216 } 1217 1218 1219 /** 1220 * Retrieve the AuthenticatingAuthority. 1221 * 1222 * @return array 1223 */ 1224 public function getAuthenticatingAuthority() : array 1225 { 1226 return $this->AuthenticatingAuthority; 1227 } 1228 1229 1230 /** 1231 * Set the AuthenticatingAuthority 1232 * 1233 * @param array $authenticatingAuthority 1234 * @return void 1235 */ 1236 public function setAuthenticatingAuthority(array $authenticatingAuthority) : void 1237 { 1238 $this->AuthenticatingAuthority = $authenticatingAuthority; 1239 } 1240 1241 1242 /** 1243 * Retrieve all attributes. 1244 * 1245 * @return array All attributes, as an associative array. 1246 */ 1247 public function getAttributes() : array 1248 { 1249 return $this->attributes; 1250 } 1251 1252 1253 /** 1254 * Replace all attributes. 1255 * 1256 * @param array $attributes All new attributes, as an associative array. 1257 * @return void 1258 */ 1259 public function setAttributes(array $attributes) : void 1260 { 1261 $this->attributes = $attributes; 1262 } 1263 1264 /** 1265 * @return array|null 1266 */ 1267 public function getSignatureData() : ?array 1268 { 1269 return $this->signatureData; 1270 } 1271 1272 1273 /** 1274 * @param array|null $signatureData 1275 * @return void 1276 */ 1277 public function setSignatureData(array $signatureData = null) : void 1278 { 1279 $this->signatureData = $signatureData; 1280 } 1281 1282 1283 /** 1284 * Retrieve all attributes value types. 1285 * 1286 * @return array All attributes value types, as an associative array. 1287 */ 1288 public function getAttributesValueTypes() : array 1289 { 1290 return $this->attributesValueTypes; 1291 } 1292 1293 1294 /** 1295 * Replace all attributes value types.. 1296 * 1297 * @param array $attributesValueTypes All new attribute value types, as an associative array. 1298 * @return void 1299 */ 1300 public function setAttributesValueTypes(array $attributesValueTypes) : void 1301 { 1302 $this->attributesValueTypes = $attributesValueTypes; 1303 } 1304 1305 1306 /** 1307 * Retrieve the NameFormat used on all attributes. 1308 * 1309 * If more than one NameFormat is used in the received attributes, this 1310 * returns the unspecified NameFormat. 1311 * 1312 * @return string The NameFormat used on all attributes. 1313 */ 1314 public function getAttributeNameFormat() : string 1315 { 1316 return $this->nameFormat; 1317 } 1318 1319 1320 /** 1321 * Set the NameFormat used on all attributes. 1322 * 1323 * @param string $nameFormat The NameFormat used on all attributes. 1324 * @return void 1325 */ 1326 public function setAttributeNameFormat(string $nameFormat) : void 1327 { 1328 $this->nameFormat = $nameFormat; 1329 } 1330 1331 1332 /** 1333 * Retrieve the SubjectConfirmation elements we have in our Subject element. 1334 * 1335 * @return array Array of \SAML2\XML\saml\SubjectConfirmation elements. 1336 */ 1337 public function getSubjectConfirmation() : array 1338 { 1339 return $this->SubjectConfirmation; 1340 } 1341 1342 1343 /** 1344 * Set the SubjectConfirmation elements that should be included in the assertion. 1345 * 1346 * @param array $SubjectConfirmation Array of \SAML2\XML\saml\SubjectConfirmation elements. 1347 * @return void 1348 */ 1349 public function setSubjectConfirmation(array $SubjectConfirmation) : void 1350 { 1351 $this->SubjectConfirmation = $SubjectConfirmation; 1352 } 1353 1354 1355 /** 1356 * Retrieve the encryptedAttributes elements we have. 1357 * 1358 * @return array Array of \DOMElement elements. 1359 */ 1360 public function getEncryptedAttributes() : array 1361 { 1362 return $this->encryptedAttributes; 1363 } 1364 1365 1366 /** 1367 * Set the encryptedAttributes elements 1368 * 1369 * @param array $encAttrs Array of \DOMElement elements. 1370 * @return void 1371 */ 1372 public function setEncryptedAttributes(array $encAttrs) : void 1373 { 1374 $this->encryptedAttributes = $encAttrs; 1375 } 1376 1377 1378 /** 1379 * Retrieve the private key we should use to sign the assertion. 1380 * 1381 * @return XMLSecurityKey|null The key, or NULL if no key is specified. 1382 */ 1383 public function getSignatureKey() : ?XMLSecurityKey 1384 { 1385 return $this->signatureKey; 1386 } 1387 1388 1389 /** 1390 * Set the private key we should use to sign the assertion. 1391 * 1392 * If the key is null, the assertion will be sent unsigned. 1393 * 1394 * @param XMLSecurityKey|null $signatureKey 1395 * @return void 1396 */ 1397 public function setSignatureKey(XMLSecurityKey $signatureKey = null) : void 1398 { 1399 $this->signatureKey = $signatureKey; 1400 } 1401 1402 1403 /** 1404 * Return the key we should use to encrypt the assertion. 1405 * 1406 * @return XMLSecurityKey|null The key, or NULL if no key is specified.. 1407 * 1408 */ 1409 public function getEncryptionKey() : ?XMLSecurityKey 1410 { 1411 return $this->encryptionKey; 1412 } 1413 1414 1415 /** 1416 * Set the private key we should use to encrypt the attributes. 1417 * 1418 * @param XMLSecurityKey|null $Key 1419 * @return void 1420 */ 1421 public function setEncryptionKey(XMLSecurityKey $Key = null) : void 1422 { 1423 $this->encryptionKey = $Key; 1424 } 1425 1426 1427 /** 1428 * Set the certificates that should be included in the assertion. 1429 * 1430 * The certificates should be strings with the PEM encoded data. 1431 * 1432 * @param array $certificates An array of certificates. 1433 * @return void 1434 */ 1435 public function setCertificates(array $certificates) : void 1436 { 1437 $this->certificates = $certificates; 1438 } 1439 1440 1441 /** 1442 * Retrieve the certificates that are included in the assertion. 1443 * 1444 * @return array An array of certificates. 1445 */ 1446 public function getCertificates() : array 1447 { 1448 return $this->certificates; 1449 } 1450 1451 1452 /** 1453 * @return bool 1454 */ 1455 public function wasSignedAtConstruction() : bool 1456 { 1457 return $this->wasSignedAtConstruction; 1458 } 1459 1460 1461 /** 1462 * Convert this assertion to an XML element. 1463 * 1464 * @param \DOMNode|null $parentElement The DOM node the assertion should be created in. 1465 * @return \DOMElement This assertion. 1466 */ 1467 public function toXML(\DOMNode $parentElement = null) : DOMElement 1468 { 1469 if ($parentElement === null) { 1470 $document = DOMDocumentFactory::create(); 1471 $parentElement = $document; 1472 } else { 1473 $document = $parentElement->ownerDocument; 1474 } 1475 1476 $root = $document->createElementNS(Constants::NS_SAML, 'saml:'.'Assertion'); 1477 $parentElement->appendChild($root); 1478 1479 /* Ugly hack to add another namespace declaration to the root element. */ 1480 $root->setAttributeNS(Constants::NS_SAMLP, 'samlp:tmp', 'tmp'); 1481 $root->removeAttributeNS(Constants::NS_SAMLP, 'tmp'); 1482 $root->setAttributeNS(Constants::NS_XSI, 'xsi:tmp', 'tmp'); 1483 $root->removeAttributeNS(Constants::NS_XSI, 'tmp'); 1484 $root->setAttributeNS(Constants::NS_XS, 'xs:tmp', 'tmp'); 1485 $root->removeAttributeNS(Constants::NS_XS, 'tmp'); 1486 1487 $root->setAttribute('ID', $this->id); 1488 $root->setAttribute('Version', '2.0'); 1489 $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant)); 1490 1491 $issuer = $this->issuer->toXML($root); 1492 1493 $this->addSubject($root); 1494 $this->addConditions($root); 1495 $this->addAuthnStatement($root); 1496 if ($this->getRequiredEncAttributes() === false) { 1497 $this->addAttributeStatement($root); 1498 } else { 1499 $this->addEncryptedAttributeStatement($root); 1500 } 1501 1502 if ($this->signatureKey !== null) { 1503 Utils::insertSignature($this->signatureKey, $this->certificates, $root, $issuer->nextSibling); 1504 } 1505 1506 return $root; 1507 } 1508 1509 1510 /** 1511 * Add a Subject-node to the assertion. 1512 * 1513 * @param \DOMElement $root The assertion element we should add the subject to. 1514 * @return void 1515 */ 1516 private function addSubject(DOMElement $root) : void 1517 { 1518 if ($this->nameId === null && $this->encryptedNameId === null) { 1519 /* We don't have anything to create a Subject node for. */ 1520 1521 return; 1522 } 1523 1524 $subject = $root->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:Subject'); 1525 $root->appendChild($subject); 1526 1527 if ($this->encryptedNameId === null) { 1528 $this->nameId->toXML($subject); 1529 } else { 1530 $eid = $subject->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:'.'EncryptedID'); 1531 $subject->appendChild($eid); 1532 $eid->appendChild($subject->ownerDocument->importNode($this->encryptedNameId, true)); 1533 } 1534 1535 foreach ($this->SubjectConfirmation as $sc) { 1536 $sc->toXML($subject); 1537 } 1538 } 1539 1540 1541 /** 1542 * Add a Conditions-node to the assertion. 1543 * 1544 * @param \DOMElement $root The assertion element we should add the conditions to. 1545 * @return void 1546 */ 1547 private function addConditions(DOMElement $root) : void 1548 { 1549 $document = $root->ownerDocument; 1550 1551 $conditions = $document->createElementNS(Constants::NS_SAML, 'saml:Conditions'); 1552 $root->appendChild($conditions); 1553 1554 if ($this->notBefore !== null) { 1555 $conditions->setAttribute('NotBefore', gmdate('Y-m-d\TH:i:s\Z', $this->notBefore)); 1556 } 1557 if ($this->notOnOrAfter !== null) { 1558 $conditions->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter)); 1559 } 1560 1561 if ($this->validAudiences !== null) { 1562 $ar = $document->createElementNS(Constants::NS_SAML, 'saml:AudienceRestriction'); 1563 $conditions->appendChild($ar); 1564 1565 Utils::addStrings($ar, Constants::NS_SAML, 'saml:Audience', false, $this->validAudiences); 1566 } 1567 } 1568 1569 1570 /** 1571 * Add a AuthnStatement-node to the assertion. 1572 * 1573 * @param \DOMElement $root The assertion element we should add the authentication statement to. 1574 * @return void 1575 */ 1576 private function addAuthnStatement(DOMElement $root) : void 1577 { 1578 if ($this->authnInstant === null || 1579 ( 1580 $this->authnContextClassRef === null && 1581 $this->authnContextDecl === null && 1582 $this->authnContextDeclRef === null 1583 ) 1584 ) { 1585 /* No authentication context or AuthnInstant => no authentication statement. */ 1586 1587 return; 1588 } 1589 1590 $document = $root->ownerDocument; 1591 1592 $authnStatementEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnStatement'); 1593 $root->appendChild($authnStatementEl); 1594 1595 $authnStatementEl->setAttribute('AuthnInstant', gmdate('Y-m-d\TH:i:s\Z', $this->authnInstant)); 1596 1597 if ($this->sessionNotOnOrAfter !== null) { 1598 $authnStatementEl->setAttribute( 1599 'SessionNotOnOrAfter', 1600 gmdate('Y-m-d\TH:i:s\Z', $this->sessionNotOnOrAfter) 1601 ); 1602 } 1603 if ($this->sessionIndex !== null) { 1604 $authnStatementEl->setAttribute('SessionIndex', $this->sessionIndex); 1605 } 1606 1607 $authnContextEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnContext'); 1608 $authnStatementEl->appendChild($authnContextEl); 1609 1610 if (!empty($this->authnContextClassRef)) { 1611 Utils::addString( 1612 $authnContextEl, 1613 Constants::NS_SAML, 1614 'saml:AuthnContextClassRef', 1615 $this->authnContextClassRef 1616 ); 1617 } 1618 if (!empty($this->authnContextDecl)) { 1619 $this->authnContextDecl->toXML($authnContextEl); 1620 } 1621 if (!empty($this->authnContextDeclRef)) { 1622 Utils::addString( 1623 $authnContextEl, 1624 Constants::NS_SAML, 1625 'saml:AuthnContextDeclRef', 1626 $this->authnContextDeclRef 1627 ); 1628 } 1629 1630 Utils::addStrings( 1631 $authnContextEl, 1632 Constants::NS_SAML, 1633 'saml:AuthenticatingAuthority', 1634 false, 1635 $this->AuthenticatingAuthority 1636 ); 1637 } 1638 1639 1640 /** 1641 * Add an AttributeStatement-node to the assertion. 1642 * 1643 * @param \DOMElement $root The assertion element we should add the subject to. 1644 * @return void 1645 */ 1646 private function addAttributeStatement(DOMElement $root) : void 1647 { 1648 if (empty($this->attributes)) { 1649 return; 1650 } 1651 1652 $document = $root->ownerDocument; 1653 1654 $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement'); 1655 $root->appendChild($attributeStatement); 1656 1657 foreach ($this->attributes as $name => $values) { 1658 $attribute = $document->createElementNS(Constants::NS_SAML, 'saml:Attribute'); 1659 $attributeStatement->appendChild($attribute); 1660 $attribute->setAttribute('Name', $name); 1661 1662 if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) { 1663 $attribute->setAttribute('NameFormat', $this->nameFormat); 1664 } 1665 1666 // make sure eduPersonTargetedID can be handled properly as a NameID 1667 if ($name === Constants::EPTI_URN_MACE || $name === Constants::EPTI_URN_OID) { 1668 foreach ($values as $eptiValue) { 1669 $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue'); 1670 $attribute->appendChild($attributeValue); 1671 if ($eptiValue instanceof NameID) { 1672 $eptiValue->toXML($attributeValue); 1673 } elseif ($eptiValue instanceof DOMNodeList) { 1674 /** @var \DOMElement $value */ 1675 $value = $eptiValue->item(0); 1676 $node = $root->ownerDocument->importNode($value, true); 1677 $attributeValue->appendChild($node); 1678 } else { 1679 $attributeValue->textContent = $eptiValue; 1680 } 1681 } 1682 1683 continue; 1684 } 1685 1686 // get value type(s) for the current attribute 1687 if (array_key_exists($name, $this->attributesValueTypes)) { 1688 $valueTypes = $this->attributesValueTypes[$name]; 1689 if (is_array($valueTypes) && count($valueTypes) != count($values)) { 1690 throw new \Exception('Array of value types and array of values have different size for attribute '. 1691 var_export($name, true)); 1692 } 1693 } else { 1694 // if no type(s), default behaviour 1695 $valueTypes = null; 1696 } 1697 1698 $vidx = -1; 1699 foreach ($values as $value) { 1700 $vidx++; 1701 1702 // try to get type from current types 1703 $type = null; 1704 if (!is_null($valueTypes)) { 1705 if (is_array($valueTypes)) { 1706 $type = $valueTypes[$vidx]; 1707 } else { 1708 $type = $valueTypes; 1709 } 1710 } 1711 1712 // if no type get from types, use default behaviour 1713 if (is_null($type)) { 1714 if (is_string($value)) { 1715 $type = 'xs:string'; 1716 } elseif (is_int($value)) { 1717 $type = 'xs:integer'; 1718 } else { 1719 $type = null; 1720 } 1721 } 1722 1723 $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue'); 1724 $attribute->appendChild($attributeValue); 1725 if ($type !== null) { 1726 $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type); 1727 } 1728 if (is_null($value)) { 1729 $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:nil', 'true'); 1730 } 1731 1732 if ($value instanceof \DOMNodeList) { 1733 foreach ($value as $v) { 1734 $node = $document->importNode($v, true); 1735 $attributeValue->appendChild($node); 1736 } 1737 } else { 1738 $value = strval($value); 1739 $attributeValue->appendChild($document->createTextNode($value)); 1740 } 1741 } 1742 } 1743 } 1744 1745 1746 /** 1747 * Add an EncryptedAttribute Statement-node to the assertion. 1748 * 1749 * @param \DOMElement $root The assertion element we should add the Encrypted Attribute Statement to. 1750 * @return void 1751 */ 1752 private function addEncryptedAttributeStatement(DOMElement $root) : void 1753 { 1754 if ($this->getRequiredEncAttributes() === false) { 1755 return; 1756 } 1757 Assert::notNull($this->encryptionKey); 1758 1759 $document = $root->ownerDocument; 1760 1761 $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement'); 1762 $root->appendChild($attributeStatement); 1763 1764 foreach ($this->attributes as $name => $values) { 1765 $document2 = DOMDocumentFactory::create(); 1766 $attribute = $document2->createElementNS(Constants::NS_SAML, 'saml:Attribute'); 1767 $attribute->setAttribute('Name', $name); 1768 $document2->appendChild($attribute); 1769 1770 if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) { 1771 $attribute->setAttribute('NameFormat', $this->getAttributeNameFormat()); 1772 } 1773 1774 foreach ($values as $value) { 1775 if (is_string($value)) { 1776 $type = 'xs:string'; 1777 } elseif (is_int($value)) { 1778 $type = 'xs:integer'; 1779 } else { 1780 $type = null; 1781 } 1782 1783 $attributeValue = $document2->createElementNS(Constants::NS_SAML, 'saml:AttributeValue'); 1784 $attribute->appendChild($attributeValue); 1785 if ($type !== null) { 1786 $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type); 1787 } 1788 1789 if ($value instanceof DOMNodeList) { 1790 foreach ($value as $v) { 1791 $node = $document2->importNode($v, true); 1792 $attributeValue->appendChild($node); 1793 } 1794 } else { 1795 $value = strval($value); 1796 $attributeValue->appendChild($document2->createTextNode($value)); 1797 } 1798 } 1799 /*Once the attribute nodes are built, the are encrypted*/ 1800 $EncAssert = new XMLSecEnc(); 1801 $EncAssert->setNode($document2->documentElement); 1802 $EncAssert->type = 'http://www.w3.org/2001/04/xmlenc#Element'; 1803 /* 1804 * Attributes are encrypted with a session key and this one with 1805 * $EncryptionKey 1806 */ 1807 $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES256_CBC); 1808 $symmetricKey->generateSessionKey(); 1809 /** @psalm-suppress PossiblyNullArgument */ 1810 $EncAssert->encryptKey($this->encryptionKey, $symmetricKey); 1811 /** @psalm-suppress UndefinedClass */ 1812 $EncrNode = $EncAssert->encryptNode($symmetricKey); 1813 1814 $EncAttribute = $document->createElementNS(Constants::NS_SAML, 'saml:EncryptedAttribute'); 1815 $attributeStatement->appendChild($EncAttribute); 1816 /** @psalm-suppress InvalidArgument */ 1817 $n = $document->importNode($EncrNode, true); 1818 $EncAttribute->appendChild($n); 1819 } 1820 } 1821} 1822