• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAML-Toolkits / php-saml / #1

pending completion
#1

push

pitbulk
Fix tests

10 of 10 new or added lines in 2 files covered. (100.0%)

1994 of 2430 relevant lines covered (82.06%)

2.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.94
/src/Saml2/Response.php
1
<?php
2
/**
3
 * This file is part of php-saml.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @package OneLogin
9
 * @author  Sixto Martin <sixto.martin.garcia@gmail.com>
10
 * @license MIT https://github.com/SAML-Toolkits/php-saml/blob/master/LICENSE
11
 * @link    https://github.com/SAML-Toolkits/php-saml
12
 */
13

14
namespace OneLogin\Saml2;
15

16
use RobRichards\XMLSecLibs\XMLSecurityKey;
17
use RobRichards\XMLSecLibs\XMLSecEnc;
18

19
use DOMDocument;
20
use DOMNodeList;
21
use DOMXPath;
22
use Exception;
23

24
/**
25
 * SAML 2 Authentication Response
26
 */
27
class Response
28
{
29
    /**
30
     * Settings
31
     *
32
     * @var Settings
33
     */
34
    protected $_settings;
35

36
    /**
37
     * The decoded, unprocessed XML response provided to the constructor.
38
     *
39
     * @var string
40
     */
41
    public $response;
42

43
    /**
44
     * A DOMDocument class loaded from the SAML Response.
45
     *
46
     * @var DOMDocument
47
     */
48
    public $document;
49

50
    /**
51
     * A DOMDocument class loaded from the SAML Response (Decrypted).
52
     *
53
     * @var DOMDocument
54
     */
55
    public $decryptedDocument;
56

57
    /**
58
     * The response contains an encrypted assertion.
59
     *
60
     * @var bool
61
     */
62
    public $encrypted = false;
63

64
    /**
65
     * After validation, if it fail this var has the cause of the problem
66
     *
67
     * @var Exception|null
68
     */
69
    private $_error;
70

71
    /**
72
     * NotOnOrAfter value of a valid SubjectConfirmationData node
73
     *
74
     * @var int
75
     */
76
    private $_validSCDNotOnOrAfter;
77

78
    /**
79
     * Constructs the SAML Response object.
80
     *
81
     * @param Settings $settings Settings.
82
     * @param string   $response A UUEncoded SAML response from the IdP.
83
     *
84
     * @throws Exception
85
     * @throws ValidationError
86
     */
87
    public function __construct(\OneLogin\Saml2\Settings $settings, $response)
88
    {
89
        $this->_settings = $settings;
4✔
90

91
        $baseURL = $this->_settings->getBaseURL();
4✔
92
        if (!empty($baseURL)) {
4✔
93
            Utils::setBaseURL($baseURL);
×
94
        }
95

96
        $this->response = base64_decode($response);
4✔
97

98
        $this->document = new DOMDocument();
4✔
99
        $this->document = Utils::loadXML($this->document, $this->response);
4✔
100
        if (!$this->document) {
4✔
101
            throw new ValidationError(
×
102
                "SAML Response could not be processed",
×
103
                ValidationError::INVALID_XML_FORMAT
×
104
            );
×
105
        }
106

107
        // Quick check for the presence of EncryptedAssertion
108
        $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion');
4✔
109
        if ($encryptedAssertionNodes->length !== 0) {
4✔
110
            $this->decryptedDocument = clone $this->document;
3✔
111
            $this->encrypted = true;
3✔
112
            $this->decryptedDocument = $this->decryptAssertion($this->decryptedDocument);
3✔
113
        }
114
    }
115

116
    /**
117
     * Determines if the SAML Response is valid using the certificate.
118
     *
119
     * @param string|null $requestId The ID of the AuthNRequest sent by this SP to the IdP
120
     *
121
     * @return bool Validate the document
122
     *
123
     * @throws Exception
124
     * @throws ValidationError
125
     */
126
    public function isValid($requestId = null)
127
    {
128
        $this->_error = null;
30✔
129
        try {
130
            // Check SAML version
131
            if ($this->document->documentElement->getAttribute('Version') != '2.0') {
30✔
132
                throw new ValidationError(
1✔
133
                    "Unsupported SAML version",
1✔
134
                    ValidationError::UNSUPPORTED_SAML_VERSION
1✔
135
                );
1✔
136
            }
137

138
            if (!$this->document->documentElement->hasAttribute('ID')) {
29✔
139
                throw new ValidationError(
1✔
140
                    "Missing ID attribute on SAML Response",
1✔
141
                    ValidationError::MISSING_ID
1✔
142
                );
1✔
143
            }
144

145
            $this->checkStatus();
28✔
146

147
            $singleAssertion = $this->validateNumAssertions();
28✔
148
            if (!$singleAssertion) {
28✔
149
                throw new ValidationError(
1✔
150
                    "SAML Response must contain 1 assertion",
1✔
151
                    ValidationError::WRONG_NUMBER_OF_ASSERTIONS
1✔
152
                );
1✔
153
            }
154

155
            $idpData = $this->_settings->getIdPData();
27✔
156
            $idPEntityId = $idpData['entityId'];
27✔
157
            $spData = $this->_settings->getSPData();
27✔
158
            $spEntityId = $spData['entityId'];
27✔
159

160
            $signedElements = $this->processSignedElements();
27✔
161

162
            $responseTag = '{'.Constants::NS_SAMLP.'}Response';
26✔
163
            $assertionTag = '{'.Constants::NS_SAML.'}Assertion';
26✔
164

165
            $hasSignedResponse = in_array($responseTag, $signedElements);
26✔
166
            $hasSignedAssertion = in_array($assertionTag, $signedElements);
26✔
167

168
            if ($this->_settings->isStrict()) {
26✔
169
                $security = $this->_settings->getSecurityData();
15✔
170

171
                if ($security['wantXMLValidation']) {
15✔
172
                    $errorXmlMsg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd";
15✔
173
                    $res = Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath());
15✔
174
                    if (!$res instanceof DOMDocument) {
15✔
175
                        throw new ValidationError(
1✔
176
                            $errorXmlMsg,
1✔
177
                            ValidationError::INVALID_XML_FORMAT
1✔
178
                        );
1✔
179
                    }
180

181
                    // If encrypted, check also the decrypted document
182
                    if ($this->encrypted) {
14✔
183
                        $res = Utils::validateXML($this->decryptedDocument, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath());
1✔
184
                        if (!$res instanceof DOMDocument) {
1✔
185
                            throw new ValidationError(
×
186
                                $errorXmlMsg,
×
187
                                ValidationError::INVALID_XML_FORMAT
×
188
                            );
×
189
                        }
190
                    }
191

192
                }
193

194
                $currentURL = Utils::getSelfRoutedURLNoQuery();
15✔
195

196
                $responseInResponseTo = null;
15✔
197
                if ($this->document->documentElement->hasAttribute('InResponseTo')) {
15✔
198
                    $responseInResponseTo = $this->document->documentElement->getAttribute('InResponseTo');
14✔
199
                }
200

201
                if (!isset($requestId) && isset($responseInResponseTo) && $security['rejectUnsolicitedResponsesWithInResponseTo']) {
15✔
202
                    throw new ValidationError(
1✔
203
                        "The Response has an InResponseTo attribute: " . $responseInResponseTo . " while no InResponseTo was expected",
1✔
204
                        ValidationError::WRONG_INRESPONSETO
1✔
205
                    );
1✔
206
                }
207

208
                // Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
209
                if (isset($requestId) && $requestId != $responseInResponseTo) {
15✔
210
                    if ($responseInResponseTo == null) {
2✔
211
                        throw new ValidationError(
1✔
212
                            "No InResponseTo at the Response, but it was provided the requestId related to the AuthNRequest sent by the SP: $requestId",
1✔
213
                            ValidationError::WRONG_INRESPONSETO
1✔
214
                        );
1✔
215
                    } else {
216
                        throw new ValidationError(
1✔
217
                            "The InResponseTo of the Response: $responseInResponseTo, does not match the ID of the AuthNRequest sent by the SP: $requestId",
1✔
218
                            ValidationError::WRONG_INRESPONSETO
1✔
219
                        );
1✔
220
                    }
221
                }
222

223
                if (!$this->encrypted && $security['wantAssertionsEncrypted']) {
15✔
224
                    throw new ValidationError(
1✔
225
                        "The assertion of the Response is not encrypted and the SP requires it",
1✔
226
                        ValidationError::NO_ENCRYPTED_ASSERTION
1✔
227
                    );
1✔
228
                }
229

230
                if ($security['wantNameIdEncrypted']) {
15✔
231
                    $encryptedIdNodes = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData');
1✔
232
                    if ($encryptedIdNodes->length != 1) {
1✔
233
                        throw new ValidationError(
1✔
234
                            "The NameID of the Response is not encrypted and the SP requires it",
1✔
235
                            ValidationError::NO_ENCRYPTED_NAMEID
1✔
236
                        );
1✔
237
                    }
238
                }
239

240
                // Validate Conditions element exists
241
                if (!$this->checkOneCondition()) {
15✔
242
                    throw new ValidationError(
×
243
                        "The Assertion must include a Conditions element",
×
244
                        ValidationError::MISSING_CONDITIONS
×
245
                    );
×
246
                }
247

248
                // Validate Asserion timestamps
249
                $this->validateTimestamps();
15✔
250

251
                // Validate AuthnStatement element exists and is unique
252
                if (!$this->checkOneAuthnStatement()) {
14✔
253
                    throw new ValidationError(
×
254
                        "The Assertion must include an AuthnStatement element",
×
255
                        ValidationError::WRONG_NUMBER_OF_AUTHSTATEMENTS
×
256
                    );
×
257
                }
258

259
                // EncryptedAttributes are not supported
260
                $encryptedAttributeNodes = $this->_queryAssertion('/saml:AttributeStatement/saml:EncryptedAttribute');
14✔
261
                if ($encryptedAttributeNodes->length > 0) {
14✔
262
                    throw new ValidationError(
1✔
263
                        "There is an EncryptedAttribute in the Response and this SP not support them",
1✔
264
                        ValidationError::ENCRYPTED_ATTRIBUTES
1✔
265
                    );
1✔
266
                }
267

268
                // Check destination
269
                if ($this->document->documentElement->hasAttribute('Destination')) {
13✔
270
                    $destination = $this->document->documentElement->getAttribute('Destination');
13✔
271
                    if (isset($destination)) {
13✔
272
                        $destination = trim($destination);
13✔
273
                    }
274
                    if (empty($destination)) {
13✔
275
                        if (!$security['relaxDestinationValidation']) {
1✔
276
                            throw new ValidationError(
1✔
277
                                "The response has an empty Destination value",
1✔
278
                                ValidationError::EMPTY_DESTINATION
1✔
279
                            );
1✔
280
                        }
281
                    } else {
282
                        $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL);
13✔
283
                        if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) {
13✔
284
                            $currentURLNoRouted = Utils::getSelfURLNoQuery();
2✔
285
                            $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted);
2✔
286
                            if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) {
2✔
287
                                throw new ValidationError(
2✔
288
                                    "The response was received at $currentURL instead of $destination",
2✔
289
                                    ValidationError::WRONG_DESTINATION
2✔
290
                                );
2✔
291
                            }
292
                        }
293
                    }
294
                }
295

296
                // Check audience
297
                $validAudiences = $this->getAudiences();
12✔
298
                if (!empty($validAudiences) && !in_array($spEntityId, $validAudiences, true)) {
12✔
299
                    $validAudiencesStr = implode(',', $validAudiences);
1✔
300
                    throw new ValidationError(
1✔
301
                        "Invalid audience for this Response (expected '".$spEntityId."', got '".$validAudiencesStr."')",
1✔
302
                        ValidationError::WRONG_AUDIENCE
1✔
303
                    );
1✔
304
                }
305

306
                // Check the issuers
307
                $issuers = $this->getIssuers();
11✔
308
                foreach ($issuers as $issuer) {
11✔
309
                    if (isset($issuer)) {
11✔
310
                        $trimmedIssuer = trim($issuer);
11✔
311
                        if (empty($trimmedIssuer) || $trimmedIssuer !== $idPEntityId) {
11✔
312
                            throw new ValidationError(
1✔
313
                                "Invalid issuer in the Assertion/Response (expected '".$idPEntityId."', got '".$trimmedIssuer."')",
1✔
314
                                ValidationError::WRONG_ISSUER
1✔
315
                            );
1✔
316
                        }
317
                    }
318
                }
319

320
                // Check the session Expiration
321
                $sessionExpiration = $this->getSessionNotOnOrAfter();
10✔
322
                if (!empty($sessionExpiration) && $sessionExpiration + Constants::ALLOWED_CLOCK_DRIFT <= time()) {
10✔
323
                    throw new ValidationError(
1✔
324
                        "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response",
1✔
325
                        ValidationError::SESSION_EXPIRED
1✔
326
                    );
1✔
327
                }
328

329
                // Check the SubjectConfirmation, at least one SubjectConfirmation must be valid
330
                $anySubjectConfirmation = false;
9✔
331
                $subjectConfirmationNodes = $this->_queryAssertion('/saml:Subject/saml:SubjectConfirmation');
9✔
332
                foreach ($subjectConfirmationNodes as $scn) {
9✔
333
                    if ($scn->hasAttribute('Method') && $scn->getAttribute('Method') != Constants::CM_BEARER) {
9✔
334
                        continue;
1✔
335
                    }
336
                    $subjectConfirmationDataNodes = $scn->getElementsByTagName('SubjectConfirmationData');
9✔
337
                    if ($subjectConfirmationDataNodes->length == 0) {
9✔
338
                        continue;
1✔
339
                    } else {
340
                        $scnData = $subjectConfirmationDataNodes->item(0);
9✔
341
                        if ($scnData->hasAttribute('InResponseTo')) {
9✔
342
                            $inResponseTo = $scnData->getAttribute('InResponseTo');
9✔
343
                            if (isset($responseInResponseTo) && $responseInResponseTo != $inResponseTo) {
9✔
344
                                continue;
1✔
345
                            }
346
                        }
347
                        if ($scnData->hasAttribute('Recipient')) {
9✔
348
                            $recipient = $scnData->getAttribute('Recipient');
8✔
349
                            if (!empty($recipient) && strpos($recipient, $currentURL) === false) {
8✔
350
                                continue;
1✔
351
                            }
352
                        }
353
                        if ($scnData->hasAttribute('NotOnOrAfter')) {
9✔
354
                            $noa = Utils::parseSAML2Time($scnData->getAttribute('NotOnOrAfter'));
9✔
355
                            if ($noa + Constants::ALLOWED_CLOCK_DRIFT <= time()) {
9✔
356
                                continue;
1✔
357
                            }
358
                        }
359
                        if ($scnData->hasAttribute('NotBefore')) {
9✔
360
                            $nb = Utils::parseSAML2Time($scnData->getAttribute('NotBefore'));
1✔
361
                            if ($nb > time() + Constants::ALLOWED_CLOCK_DRIFT) {
1✔
362
                                continue;
1✔
363
                            }
364
                        }
365

366
                        // Save NotOnOrAfter value
367
                        if ($scnData->hasAttribute('NotOnOrAfter')) {
8✔
368
                            $this->_validSCDNotOnOrAfter = $noa;
8✔
369
                        }
370
                        $anySubjectConfirmation = true;
8✔
371
                        break;
8✔
372
                    }
373
                }
374

375
                if (!$anySubjectConfirmation) {
9✔
376
                    throw new ValidationError(
1✔
377
                        "A valid SubjectConfirmation was not found on this Response",
1✔
378
                        ValidationError::WRONG_SUBJECTCONFIRMATION
1✔
379
                    );
1✔
380
                }
381

382
                if ($security['wantAssertionsSigned'] && !$hasSignedAssertion) {
8✔
383
                    throw new ValidationError(
1✔
384
                        "The Assertion of the Response is not signed and the SP requires it",
1✔
385
                        ValidationError::NO_SIGNED_ASSERTION
1✔
386
                    );
1✔
387
                }
388

389
                if ($security['wantMessagesSigned'] && !$hasSignedResponse) {
8✔
390
                    throw new ValidationError(
1✔
391
                        "The Message of the Response is not signed and the SP requires it",
1✔
392
                        ValidationError::NO_SIGNED_MESSAGE
1✔
393
                    );
1✔
394
                }
395
            }
396

397
            // Detect case not supported
398
            if ($this->encrypted) {
26✔
399
                $encryptedIDNodes = Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID');
1✔
400
                if ($encryptedIDNodes->length > 0) {
1✔
401
                    throw new ValidationError(
×
402
                        'SAML Response that contains an encrypted Assertion with encrypted nameId is not supported.',
×
403
                        ValidationError::NOT_SUPPORTED
×
404
                    );
×
405
                }
406
            }
407

408
            if (empty($signedElements) || (!$hasSignedResponse && !$hasSignedAssertion)) {
26✔
409
                throw new ValidationError(
15✔
410
                    'No Signature found. SAML Response rejected',
15✔
411
                    ValidationError::NO_SIGNATURE_FOUND
15✔
412
                );
15✔
413
            } else {
414
                $cert = $idpData['x509cert'];
13✔
415
                $fingerprint = $idpData['certFingerprint'];
13✔
416
                $fingerprintalg = $idpData['certFingerprintAlgorithm'];
13✔
417

418
                $multiCerts = null;
13✔
419
                $existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']);
13✔
420

421
                if ($existsMultiX509Sign) {
13✔
422
                    $multiCerts = $idpData['x509certMulti']['signing'];
1✔
423
                }
424

425
                // If find a Signature on the Response, validates it checking the original response
426
                if ($hasSignedResponse && !Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, Utils::RESPONSE_SIGNATURE_XPATH, $multiCerts)) {
13✔
427
                    throw new ValidationError(
1✔
428
                        "Signature validation failed. SAML Response rejected",
1✔
429
                        ValidationError::INVALID_SIGNATURE
1✔
430
                    );
1✔
431
                }
432

433
                // If find a Signature on the Assertion (decrypted assertion if was encrypted)
434
                $documentToCheckAssertion = $this->encrypted ? $this->decryptedDocument : $this->document;
11✔
435
                if ($hasSignedAssertion && !Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, Utils::ASSERTION_SIGNATURE_XPATH, $multiCerts)) {
11✔
436
                    throw new ValidationError(
×
437
                        "Signature validation failed. SAML Response rejected",
×
438
                        ValidationError::INVALID_SIGNATURE
×
439
                    );
×
440
                }
441
            }
442
            return true;
9✔
443
        } catch (Exception $e) {
26✔
444
            $this->_error = $e;
26✔
445
            $debug = $this->_settings->isDebugActive();
26✔
446
            if ($debug) {
26✔
447
                echo htmlentities($e->getMessage());
×
448
            }
449
            return false;
26✔
450
        }
451
    }
452

453
    /**
454
     * @return string|null the ID of the Response
455
     */
456
    public function getId()
457
    {
458
        $id = null;
1✔
459
        if ($this->document->documentElement->hasAttribute('ID')) {
1✔
460
            $id = $this->document->documentElement->getAttribute('ID');
1✔
461
        }
462
        return $id;
1✔
463
    }
464

465
    /**
466
     * @return string|null the ID of the assertion in the Response
467
     *
468
     * @throws ValidationError
469
     */
470
    public function getAssertionId()
471
    {
472
        if (!$this->validateNumAssertions()) {
1✔
473
            throw new ValidationError("SAML Response must contain 1 Assertion.", ValidationError::WRONG_NUMBER_OF_ASSERTIONS);
×
474
        }
475
        $assertionNodes = $this->_queryAssertion("");
1✔
476
        $id = null;
1✔
477
        if ($assertionNodes->length == 1 && $assertionNodes->item(0)->hasAttribute('ID')) {
1✔
478
            $id = $assertionNodes->item(0)->getAttribute('ID');
1✔
479
        }
480
        return $id;
1✔
481
    }
482

483
    /**
484
     * @return int the NotOnOrAfter value of the valid SubjectConfirmationData
485
     * node if any
486
     */
487
    public function getAssertionNotOnOrAfter()
488
    {
489
        return $this->_validSCDNotOnOrAfter;
×
490
    }
491

492
    /**
493
     * Checks if the Status is success
494
     *
495
     * @throws ValidationError If status is not success
496
     */
497
    public function checkStatus()
498
    {
499
        $status = Utils::getStatus($this->document);
3✔
500

501
        if (isset($status['code']) && $status['code'] !== Constants::STATUS_SUCCESS) {
3✔
502
            $explodedCode = explode(':', $status['code']);
1✔
503
            $printableCode = array_pop($explodedCode);
1✔
504

505
            $statusExceptionMsg = 'The status code of the Response was not Success, was '.$printableCode;
1✔
506
            if (!empty($status['msg'])) {
1✔
507
                $statusExceptionMsg .= ' -> '.$status['msg'];
1✔
508
            }
509
            throw new ValidationError(
1✔
510
                $statusExceptionMsg,
1✔
511
                ValidationError::STATUS_CODE_IS_NOT_SUCCESS
1✔
512
            );
1✔
513
        }
514
    }
515

516
    /**
517
     * Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
518
     *
519
     * @return boolean true if the Conditions element exists and is unique
520
     */
521
    public function checkOneCondition()
522
    {
523
        $entries = $this->_queryAssertion("/saml:Conditions");
1✔
524
        if ($entries->length == 1) {
1✔
525
            return true;
1✔
526
        } else {
527
            return false;
1✔
528
        }
529
    }
530

531
    /**
532
     * Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
533
     *
534
     * @return boolean true if the AuthnStatement element exists and is unique
535
     */
536
    public function checkOneAuthnStatement()
537
    {
538
        $entries = $this->_queryAssertion("/saml:AuthnStatement");
1✔
539
        if ($entries->length == 1) {
1✔
540
            return true;
1✔
541
        } else {
542
            return false;
1✔
543
        }
544
    }
545

546
    /**
547
     * Gets the audiences.
548
     *
549
     * @return array @audience The valid audiences of the response
550
     */
551
    public function getAudiences()
552
    {
553
        $audiences = array();
1✔
554

555
        $entries = $this->_queryAssertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience');
1✔
556
        foreach ($entries as $entry) {
1✔
557
            $value = $entry->textContent;
1✔
558
            if (isset($value)) {
1✔
559
                $value = trim($value);
1✔
560
            }
561
            if (!empty($value)) {
1✔
562
                $audiences[] = $value;
1✔
563
            }
564
        }
565

566
        return array_unique($audiences);
1✔
567
    }
568

569
    /**
570
     * Gets the Issuers (from Response and Assertion).
571
     *
572
     * @return array @issuers The issuers of the assertion/response
573
     *
574
     * @throws ValidationError
575
     */
576
    public function getIssuers()
577
    {
578
        $issuers = array();
2✔
579

580
        $responseIssuer = Utils::query($this->document, '/samlp:Response/saml:Issuer');
2✔
581
        if ($responseIssuer->length > 0) {
2✔
582
            if ($responseIssuer->length == 1) {
2✔
583
                $issuers[] = $responseIssuer->item(0)->textContent;
2✔
584
            } else {
585
                throw new ValidationError(
×
586
                    "Issuer of the Response is multiple.",
×
587
                    ValidationError::ISSUER_MULTIPLE_IN_RESPONSE
×
588
                );
×
589
            }
590
        }
591

592
        $assertionIssuer = $this->_queryAssertion('/saml:Issuer');
2✔
593
        if ($assertionIssuer->length == 1) {
2✔
594
            $issuers[] = $assertionIssuer->item(0)->textContent;
2✔
595
        } else {
596
            throw new ValidationError(
1✔
597
                "Issuer of the Assertion not found or multiple.",
1✔
598
                ValidationError::ISSUER_NOT_FOUND_IN_ASSERTION
1✔
599
            );
1✔
600
        }
601

602
        return array_unique($issuers);
2✔
603
    }
604

605
    /**
606
     * Gets the NameID Data provided by the SAML response from the IdP.
607
     *
608
     * @return array Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
609
     *
610
     * @throws ValidationError
611
     */
612
    public function getNameIdData()
613
    {
614
        $encryptedIdDataEntries = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData');
1✔
615

616
        if ($encryptedIdDataEntries->length == 1) {
1✔
617
            $encryptedData = $encryptedIdDataEntries->item(0);
1✔
618

619
            $key = $this->_settings->getSPkey();
1✔
620
            $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private'));
1✔
621
            $seckey->loadKey($key);
1✔
622

623
            $nameId = Utils::decryptElement($encryptedData, $seckey);
1✔
624

625
        } else {
626
            $entries = $this->_queryAssertion('/saml:Subject/saml:NameID');
1✔
627
            if ($entries->length == 1) {
1✔
628
                $nameId = $entries->item(0);
1✔
629
            }
630
        }
631

632
        $nameIdData = array();
1✔
633

634
        if (!isset($nameId)) {
1✔
635
            $security = $this->_settings->getSecurityData();
1✔
636
            if ($security['wantNameId']) {
1✔
637
                throw new ValidationError(
1✔
638
                    "NameID not found in the assertion of the Response",
1✔
639
                    ValidationError::NO_NAMEID
1✔
640
                );
1✔
641
            }
642
        } else {
643
            if ($this->_settings->isStrict() && empty($nameId->nodeValue)) {
1✔
644
                throw new ValidationError(
1✔
645
                    "An empty NameID value found",
1✔
646
                    ValidationError::EMPTY_NAMEID
1✔
647
                );
1✔
648
            }
649
            $nameIdData['Value'] = $nameId->nodeValue;
1✔
650

651
            foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) {
1✔
652
                if ($nameId->hasAttribute($attr)) {
1✔
653
                    if ($this->_settings->isStrict() && $attr == 'SPNameQualifier') {
1✔
654
                        $spData = $this->_settings->getSPData();
1✔
655
                        $spEntityId = $spData['entityId'];
1✔
656
                        if ($spEntityId != $nameId->getAttribute($attr)) {
1✔
657
                            throw new ValidationError(
1✔
658
                                "The SPNameQualifier value mistmatch the SP entityID value.",
1✔
659
                                ValidationError::SP_NAME_QUALIFIER_NAME_MISMATCH
1✔
660
                            );
1✔
661
                        }
662
                    }
663
                    $nameIdData[$attr] = $nameId->getAttribute($attr);
1✔
664
                }
665
            }
666
        }
667

668
        return $nameIdData;
1✔
669
    }
670

671
    /**
672
     * Gets the NameID provided by the SAML response from the IdP.
673
     *
674
     * @return string|null Name ID Value
675
     *
676
     * @throws ValidationError
677
     */
678
    public function getNameId()
679
    {
680
        $nameIdvalue = null;
6✔
681
        $nameIdData = $this->getNameIdData();
6✔
682
        if (!empty($nameIdData) && isset($nameIdData['Value'])) {
6✔
683
            $nameIdvalue = $nameIdData['Value'];
6✔
684
        }
685
        return $nameIdvalue;
6✔
686
    }
687

688
    /**
689
     * Gets the NameID Format provided by the SAML response from the IdP.
690
     *
691
     * @return string|null Name ID Format
692
     *
693
     * @throws ValidationError
694
     */
695
    public function getNameIdFormat()
696
    {
697
        $nameIdFormat = null;
1✔
698
        $nameIdData = $this->getNameIdData();
1✔
699
        if (!empty($nameIdData) && isset($nameIdData['Format'])) {
1✔
700
            $nameIdFormat = $nameIdData['Format'];
1✔
701
        }
702
        return $nameIdFormat;
1✔
703
    }
704

705
    /**
706
     * Gets the NameID NameQualifier provided by the SAML response from the IdP.
707
     *
708
     * @return string|null Name ID NameQualifier
709
     *
710
     * @throws ValidationError
711
     */
712
    public function getNameIdNameQualifier()
713
    {
714
        $nameIdNameQualifier = null;
1✔
715
        $nameIdData = $this->getNameIdData();
1✔
716
        if (!empty($nameIdData) && isset($nameIdData['NameQualifier'])) {
1✔
717
            $nameIdNameQualifier = $nameIdData['NameQualifier'];
1✔
718
        }
719
        return $nameIdNameQualifier;
1✔
720
    }
721

722
    /**
723
     * Gets the NameID SP NameQualifier provided by the SAML response from the IdP.
724
     *
725
     * @return string|null NameID SP NameQualifier
726
     *
727
     * @throws ValidationError
728
     */
729
    public function getNameIdSPNameQualifier()
730
    {
731
        $nameIdSPNameQualifier = null;
1✔
732
        $nameIdData = $this->getNameIdData();
1✔
733
        if (!empty($nameIdData) && isset($nameIdData['SPNameQualifier'])) {
1✔
734
            $nameIdSPNameQualifier = $nameIdData['SPNameQualifier'];
1✔
735
        }
736
        return $nameIdSPNameQualifier;
1✔
737
    }
738

739
    /**
740
     * Gets the SessionNotOnOrAfter from the AuthnStatement.
741
     * Could be used to set the local session expiration
742
     *
743
     * @return int|null The SessionNotOnOrAfter value
744
     *
745
     * @throws Exception
746
     */
747
    public function getSessionNotOnOrAfter()
748
    {
749
        $notOnOrAfter = null;
1✔
750
        $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionNotOnOrAfter]');
1✔
751
        if ($entries->length !== 0) {
1✔
752
            $notOnOrAfter = Utils::parseSAML2Time($entries->item(0)->getAttribute('SessionNotOnOrAfter'));
1✔
753
        }
754
        return $notOnOrAfter;
1✔
755
    }
756

757
    /**
758
     * Gets the SessionIndex from the AuthnStatement.
759
     * Could be used to be stored in the local session in order
760
     * to be used in a future Logout Request that the SP could
761
     * send to the SP, to set what specific session must be deleted
762
     *
763
     * @return string|null The SessionIndex value
764
     */
765
    public function getSessionIndex()
766
    {
767
        $sessionIndex = null;
1✔
768
        $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionIndex]');
1✔
769
        if ($entries->length !== 0) {
1✔
770
            $sessionIndex = $entries->item(0)->getAttribute('SessionIndex');
1✔
771
        }
772
        return $sessionIndex;
1✔
773
    }
774

775
    /**
776
     * Gets the Attributes from the AttributeStatement element.
777
     *
778
     * @return array The attributes of the SAML Assertion
779
     *
780
     * @throws ValidationError
781
     */
782
    public function getAttributes()
783
    {
784
        return $this->_getAttributesByKeyName('Name');
4✔
785
    }
786

787
    /**
788
     * Gets the Attributes from the AttributeStatement element using their FriendlyName.
789
     *
790
     * @return array The attributes of the SAML Assertion
791
     *
792
     * @throws ValidationError
793
     */
794
    public function getAttributesWithFriendlyName()
795
    {
796
        return $this->_getAttributesByKeyName('FriendlyName');
1✔
797
    }
798

799
    /**
800
     * @param string $keyName
801
     *
802
     * @return array
803
     *
804
     * @throws ValidationError
805
     */
806
    private function _getAttributesByKeyName($keyName = "Name")
807
    {
808
        $attributes = array();
1✔
809
        $entries = $this->_queryAssertion('/saml:AttributeStatement/saml:Attribute');
1✔
810

811
        $security = $this->_settings->getSecurityData();
1✔
812
        $allowRepeatAttributeName = $security['allowRepeatAttributeName'];
1✔
813
        /** @var $entry DOMNode */
814
        foreach ($entries as $entry) {
1✔
815
            $attributeKeyNode = $entry->attributes->getNamedItem($keyName);
1✔
816
            if ($attributeKeyNode === null) {
1✔
817
                continue;
×
818
            }
819
            $attributeKeyName = $attributeKeyNode->nodeValue;
1✔
820
            if (in_array($attributeKeyName, array_keys($attributes), true)) {
1✔
821
                if (!$allowRepeatAttributeName) {
×
822
                    throw new ValidationError(
×
823
                        "Found an Attribute element with duplicated ".$keyName,
×
824
                        ValidationError::DUPLICATED_ATTRIBUTE_NAME_FOUND
×
825
                    );
×
826
                }
827
            }
828
            $attributeValues = array();
1✔
829
            foreach ($entry->childNodes as $childNode) {
1✔
830
                $tagName = ($childNode->prefix ? $childNode->prefix.':' : '') . 'AttributeValue';
1✔
831
                if ($childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName === $tagName) {
1✔
832
                    $attributeValues[] = $childNode->nodeValue;
1✔
833
                }
834
            }
835

836
            if (in_array($attributeKeyName, array_keys($attributes), true)) {
1✔
837
                $attributes[$attributeKeyName] = array_merge($attributes[$attributeKeyName], $attributeValues);
×
838
            } else {
839
                $attributes[$attributeKeyName] = $attributeValues;
1✔
840
            }
841
        }
842
        return $attributes;
1✔
843
    }
844

845
    /**
846
     * Verifies that the document only contains a single Assertion (encrypted or not).
847
     *
848
     * @return bool TRUE if the document passes.
849
     */
850
    public function validateNumAssertions()
851
    {
852
        $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion');
3✔
853
        $assertionNodes = $this->document->getElementsByTagName('Assertion');
3✔
854

855
        $valid = $assertionNodes->length + $encryptedAssertionNodes->length == 1;
3✔
856

857
        if ($this->encrypted) {
3✔
858
            $assertionNodes = $this->decryptedDocument->getElementsByTagName('Assertion');
1✔
859
            $valid = $valid && $assertionNodes->length == 1;
1✔
860
        }
861

862
        return $valid;
3✔
863
    }
864

865
    /**
866
     * Verifies the signature nodes:
867
     *   - Checks that are Response or Assertion
868
     *   - Check that IDs and reference URI are unique and consistent.
869
     *
870
     * @return array Signed element tags
871
     *
872
     * @throws ValidationError
873
     */
874
    public function processSignedElements()
875
    {
876
        $signedElements = array();
2✔
877
        $verifiedSeis = array();
2✔
878
        $verifiedIds = array();
2✔
879

880
        if ($this->encrypted) {
2✔
881
            $signNodes = $this->decryptedDocument->getElementsByTagName('Signature');
1✔
882
        } else {
883
            $signNodes = $this->document->getElementsByTagName('Signature');
1✔
884
        }
885
        foreach ($signNodes as $signNode) {
2✔
886
            $responseTag = '{'.Constants::NS_SAMLP.'}Response';
2✔
887
            $assertionTag = '{'.Constants::NS_SAML.'}Assertion';
2✔
888

889
            $signedElement = '{'.$signNode->parentNode->namespaceURI.'}'.$signNode->parentNode->localName;
2✔
890

891
            if ($signedElement != $responseTag && $signedElement != $assertionTag) {
2✔
892
                throw new ValidationError(
×
893
                    "Invalid Signature Element $signedElement SAML Response rejected",
×
894
                    ValidationError::WRONG_SIGNED_ELEMENT
×
895
                );
×
896
            }
897

898
            // Check that reference URI matches the parent ID and no duplicate References or IDs
899
            $idValue = $signNode->parentNode->getAttribute('ID');
2✔
900
            if (empty($idValue)) {
2✔
901
                throw new ValidationError(
×
902
                    'Signed Element must contain an ID. SAML Response rejected',
×
903
                    ValidationError::ID_NOT_FOUND_IN_SIGNED_ELEMENT
×
904
                );
×
905
            }
906

907
            if (in_array($idValue, $verifiedIds)) {
2✔
908
                throw new ValidationError(
×
909
                    'Duplicated ID. SAML Response rejected',
×
910
                    ValidationError::DUPLICATED_ID_IN_SIGNED_ELEMENTS
×
911
                );
×
912
            }
913
            $verifiedIds[] = $idValue;
2✔
914

915
            $ref = $signNode->getElementsByTagName('Reference');
2✔
916
            if ($ref->length == 1) {
2✔
917
                $ref = $ref->item(0);
2✔
918
                $sei = $ref->getAttribute('URI');
2✔
919
                if (!empty($sei)) {
2✔
920
                    $sei = substr($sei, 1);
1✔
921

922
                    if ($sei != $idValue) {
1✔
923
                        throw new ValidationError(
1✔
924
                            'Found an invalid Signed Element. SAML Response rejected',
1✔
925
                            ValidationError::INVALID_SIGNED_ELEMENT
1✔
926
                        );
1✔
927
                    }
928

929
                    if (in_array($sei, $verifiedSeis)) {
×
930
                        throw new ValidationError(
×
931
                            'Duplicated Reference URI. SAML Response rejected',
×
932
                            ValidationError::DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS
×
933
                        );
×
934
                    }
935
                    $verifiedSeis[] = $sei;
1✔
936
                }
937
            } else {
938
                throw new ValidationError(
×
939
                    'Unexpected number of Reference nodes found for signature. SAML Response rejected.',
×
940
                    ValidationError::UNEXPECTED_REFERENCE
×
941
                );
×
942
            }
943
            $signedElements[] = $signedElement;
1✔
944
        }
945

946
        // Check SignedElements
947
        if (!empty($signedElements) && !$this->validateSignedElements($signedElements)) {
1✔
948
            throw new ValidationError(
×
949
                'Found an unexpected Signature Element. SAML Response rejected',
×
950
                ValidationError::UNEXPECTED_SIGNED_ELEMENTS
×
951
            );
×
952
        }
953
        return $signedElements;
1✔
954
    }
955

956
    /**
957
     * Verifies that the document is still valid according Conditions Element.
958
     *
959
     * @return bool
960
     *
961
     * @throws Exception
962
     * @throws ValidationError
963
     */
964
    public function validateTimestamps()
965
    {
966
        if ($this->encrypted) {
1✔
967
            $document = $this->decryptedDocument;
1✔
968
        } else {
969
            $document = $this->document;
1✔
970
        }
971

972
        $timestampNodes = $document->getElementsByTagName('Conditions');
1✔
973
        for ($i = 0; $i < $timestampNodes->length; $i++) {
1✔
974
            $nbAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotBefore");
1✔
975
            $naAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotOnOrAfter");
1✔
976
            if ($nbAttribute && Utils::parseSAML2Time($nbAttribute->textContent) > time() + Constants::ALLOWED_CLOCK_DRIFT) {
1✔
977
                throw new ValidationError(
1✔
978
                    'Could not validate timestamp: not yet valid. Check system clock.',
1✔
979
                    ValidationError::ASSERTION_TOO_EARLY
1✔
980
                );
1✔
981
            }
982
            if ($naAttribute && Utils::parseSAML2Time($naAttribute->textContent) + Constants::ALLOWED_CLOCK_DRIFT <= time()) {
1✔
983
                throw new ValidationError(
1✔
984
                    'Could not validate timestamp: expired. Check system clock.',
1✔
985
                    ValidationError::ASSERTION_EXPIRED
1✔
986
                );
1✔
987
            }
988
        }
989
        return true;
1✔
990
    }
991

992
    /**
993
     * Verifies that the document has the expected signed nodes.
994
     *
995
     * @param array $signedElements Signed elements
996
     *
997
     * @return bool
998
     *
999
     * @throws ValidationError
1000
     */
1001
    public function validateSignedElements($signedElements)
1002
    {
1003
        if (count($signedElements) > 2) {
1✔
1004
            return false;
×
1005
        }
1006

1007
        $responseTag = '{'.Constants::NS_SAMLP.'}Response';
1✔
1008
        $assertionTag = '{'.Constants::NS_SAML.'}Assertion';
1✔
1009

1010
        $ocurrence = array_count_values($signedElements);
1✔
1011
        if ((in_array($responseTag, $signedElements) && $ocurrence[$responseTag] > 1)
1✔
1012
            || (in_array($assertionTag, $signedElements) && $ocurrence[$assertionTag] > 1)
1✔
1013
            || !in_array($responseTag, $signedElements) && !in_array($assertionTag, $signedElements)
1✔
1014
        ) {
1015
            return false;
×
1016
        }
1017

1018
        // Check that the signed elements found here, are the ones that will be verified
1019
        // by Utils->validateSign()
1020
        if (in_array($responseTag, $signedElements)) {
1✔
1021
            $expectedSignatureNodes = Utils::query($this->document, Utils::RESPONSE_SIGNATURE_XPATH);
1✔
1022
            if ($expectedSignatureNodes->length != 1) {
1✔
1023
                throw new ValidationError(
×
1024
                    "Unexpected number of Response signatures found. SAML Response rejected.",
×
1025
                    ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE
×
1026
                );
×
1027
            }
1028
        }
1029

1030
        if (in_array($assertionTag, $signedElements)) {
1✔
1031
            $expectedSignatureNodes = $this->_query(Utils::ASSERTION_SIGNATURE_XPATH);
×
1032
            if ($expectedSignatureNodes->length != 1) {
×
1033
                throw new ValidationError(
×
1034
                    "Unexpected number of Assertion signatures found. SAML Response rejected.",
×
1035
                    ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION
×
1036
                );
×
1037
            }
1038
        }
1039

1040
        return true;
1✔
1041
    }
1042

1043
    /**
1044
     * Extracts a node from the DOMDocument (Assertion).
1045
     *
1046
     * @param string $assertionXpath Xpath Expression
1047
     *
1048
     * @return DOMNodeList The queried node
1049
     */
1050
    protected function _queryAssertion($assertionXpath)
1051
    {
1052
        if ($this->encrypted) {
2✔
1053
            $xpath = new DOMXPath($this->decryptedDocument);
1✔
1054
        } else {
1055
            $xpath = new DOMXPath($this->document);
2✔
1056
        }
1057

1058
        $xpath->registerNamespace('samlp', Constants::NS_SAMLP);
2✔
1059
        $xpath->registerNamespace('saml', Constants::NS_SAML);
2✔
1060
        $xpath->registerNamespace('ds', Constants::NS_DS);
2✔
1061
        $xpath->registerNamespace('xenc', Constants::NS_XENC);
2✔
1062

1063
        $assertionNode = '/samlp:Response/saml:Assertion';
2✔
1064
        $signatureQuery = $assertionNode . '/ds:Signature/ds:SignedInfo/ds:Reference';
2✔
1065
        $assertionReferenceNode = $xpath->query($signatureQuery)->item(0);
2✔
1066
        if (!$assertionReferenceNode) {
2✔
1067
            // is the response signed as a whole?
1068
            $signatureQuery = '/samlp:Response/ds:Signature/ds:SignedInfo/ds:Reference';
2✔
1069
            $responseReferenceNode = $xpath->query($signatureQuery)->item(0);
2✔
1070
            if ($responseReferenceNode) {
2✔
1071
                $uri = $responseReferenceNode->attributes->getNamedItem('URI')->nodeValue;
2✔
1072
                if (empty($uri)) {
2✔
1073
                    $id = $responseReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue;
1✔
1074
                } else {
1075
                    $id = substr($responseReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1);
1✔
1076
                }
1077
                $nameQuery = "/samlp:Response[@ID='$id']/saml:Assertion" . $assertionXpath;
2✔
1078
            } else {
1079
                $nameQuery = "/samlp:Response/saml:Assertion" . $assertionXpath;
2✔
1080
            }
1081
        } else {
1082
            $uri = $assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue;
1✔
1083
            if (empty($uri)) {
1✔
1084
                $id = $assertionReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue;
×
1085
            } else {
1086
                $id = substr($assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1);
1✔
1087
            }
1088
            $nameQuery = $assertionNode."[@ID='$id']" . $assertionXpath;
1✔
1089
        }
1090

1091
        return $xpath->query($nameQuery);
2✔
1092
    }
1093

1094
    /**
1095
     * Extracts nodes that match the query from the DOMDocument (Response Menssage)
1096
     *
1097
     * @param string $query Xpath Expression
1098
     *
1099
     * @return DOMNodeList The queried nodes
1100
     */
1101
    private function _query($query)
1102
    {
1103
        if ($this->encrypted) {
×
1104
            return Utils::query($this->decryptedDocument, $query);
×
1105
        } else {
1106
            return Utils::query($this->document, $query);
×
1107
        }
1108
    }
1109

1110
    /**
1111
     * Decrypts the Assertion (DOMDocument)
1112
     *
1113
     * @param \DomNode $dom DomDocument
1114
     *
1115
     * @return DOMDocument Decrypted Assertion
1116
     *
1117
     * @throws Exception
1118
     * @throws ValidationError
1119
     */
1120
    protected function decryptAssertion(\DomNode $dom)
1121
    {
1122
        $pem = $this->_settings->getSPkey();
3✔
1123

1124
        if (empty($pem)) {
3✔
1125
            throw new Error(
×
1126
                "No private key available, check settings",
×
1127
                Error::PRIVATE_KEY_NOT_FOUND
×
1128
            );
×
1129
        }
1130

1131
        $objenc = new XMLSecEnc();
3✔
1132
        $encData = $objenc->locateEncryptedData($dom);
3✔
1133
        if (!$encData) {
3✔
1134
            throw new ValidationError(
×
1135
                "Cannot locate encrypted assertion",
×
1136
                ValidationError::MISSING_ENCRYPTED_ELEMENT
×
1137
            );
×
1138
        }
1139

1140
        $objenc->setNode($encData);
3✔
1141
        $objenc->type = $encData->getAttribute("Type");
3✔
1142
        if (!$objKey = $objenc->locateKey()) {
3✔
1143
            throw new ValidationError(
×
1144
                "Unknown algorithm",
×
1145
                ValidationError::KEY_ALGORITHM_ERROR
×
1146
            );
×
1147
        }
1148

1149
        $key = null;
3✔
1150
        if ($objKeyInfo = $objenc->locateKeyInfo($objKey)) {
3✔
1151
            if ($objKeyInfo->isEncrypted) {
3✔
1152
                $objencKey = $objKeyInfo->encryptedCtx;
3✔
1153
                $objKeyInfo->loadKey($pem, false, false);
3✔
1154
                $key = $objencKey->decryptKey($objKeyInfo);
3✔
1155
            } else {
1156
                // symmetric encryption key support
1157
                $objKeyInfo->loadKey($pem, false, false);
×
1158
            }
1159
        }
1160

1161
        if (empty($objKey->key)) {
3✔
1162
            $objKey->loadKey($key);
3✔
1163
        }
1164

1165
        $decryptedXML = $objenc->decryptNode($objKey, false);
3✔
1166
        $decrypted = new DOMDocument();
3✔
1167
        $check = Utils::loadXML($decrypted, $decryptedXML);
3✔
1168
        if ($check === false) {
3✔
1169
            throw new Exception('Error: string from decrypted assertion could not be loaded into a XML document');
×
1170
        }
1171
        if ($encData->parentNode instanceof DOMDocument) {
3✔
1172
            return $decrypted;
×
1173
        } else {
1174
            $decrypted = $decrypted->documentElement;
3✔
1175
            $encryptedAssertion = $encData->parentNode;
3✔
1176
            $container = $encryptedAssertion->parentNode;
3✔
1177

1178
            // Fix possible issue with saml namespace
1179
            if (!$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml')
3✔
1180
                && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2')
3✔
1181
                && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns')
3✔
1182
                && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml')
3✔
1183
                && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2')
3✔
1184
            ) {
1185
                if (strpos($encryptedAssertion->tagName, 'saml2:') !== false) {
3✔
1186
                    $ns = 'xmlns:saml2';
×
1187
                } else if (strpos($encryptedAssertion->tagName, 'saml:') !== false) {
3✔
1188
                    $ns = 'xmlns:saml';
3✔
1189
                } else {
1190
                    $ns = 'xmlns';
×
1191
                }
1192
                $decrypted->setAttributeNS('http://www.w3.org/2000/xmlns/', $ns, Constants::NS_SAML);
3✔
1193
            }
1194

1195
            Utils::treeCopyReplace($encryptedAssertion, $decrypted);
3✔
1196

1197
            // Rebuild the DOM will fix issues with namespaces as well
1198
            $dom = new DOMDocument();
3✔
1199
            return Utils::loadXML($dom, $container->ownerDocument->saveXML());
3✔
1200
        }
1201
    }
1202

1203
    /**
1204
     * After execute a validation process, if fails this method returns the cause
1205
     *
1206
     * @return Exception|null Cause
1207
     */
1208
    public function getErrorException()
1209
    {
1210
        return $this->_error;
×
1211
    }
1212

1213
    /**
1214
     * After execute a validation process, if fails this method returns the cause
1215
     *
1216
     * @param bool $escape Apply or not htmlentities to the message.
1217
     *
1218
     * @return null|string Error reason
1219
     */
1220
    public function getError($escape = true)
1221
    {
1222
        $errorMsg = null;
×
1223
        if (isset($this->_error)) {
×
1224
            if ($escape) {
×
1225
                $errorMsg = htmlentities($this->_error->getMessage());
×
1226
            } else {
1227
                $errorMsg = $this->_error->getMessage();
×
1228
            }
1229
        }
1230
        return $errorMsg;
×
1231
    }
1232

1233
    /**
1234
     * Returns the SAML Response document (If contains an encrypted assertion, decrypts it)
1235
     *
1236
     * @return DomDocument SAML Response
1237
     */
1238
    public function getXMLDocument()
1239
    {
1240
        if ($this->encrypted) {
1✔
1241
            return $this->decryptedDocument;
1✔
1242
        } else {
1243
            return $this->document;
1✔
1244
        }
1245
    }
1246
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc