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