Completed
Push — master ( 8e06a1...b0dbe4 )
by Jaime Pérez
05:41
created

Assertion::getCertificates()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
3
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecEnc;
6
use RobRichards\XMLSecLibs\XMLSecurityKey;
7
use SAML2\Exception\RuntimeException;
8
use SAML2\Utilities\Temporal;
9
use SAML2\XML\Chunk;
10
use SAML2\XML\saml\SubjectConfirmation;
11
12
/**
13
 * Class representing a SAML 2 assertion.
14
 *
15
 * @package SimpleSAMLphp
16
 */
17
class Assertion implements SignedElement
18
{
19
    /**
20
     * The identifier of this assertion.
21
     *
22
     * @var string
23
     */
24
    private $id;
25
26
    /**
27
     * The issue timestamp of this assertion, as an UNIX timestamp.
28
     *
29
     * @var int
30
     */
31
    private $issueInstant;
32
33
    /**
34
     * The issuer of this assertion.
35
     *
36
     * If the issuer's format is \SAML2\Constants::NAMEID_ENTITY, this property will just take the issuer's string
37
     * value.
38
     *
39
     * @var string|\SAML2\XML\saml\Issuer
40
     */
41
    private $issuer;
42
43
    /**
44
     * The NameId of the subject in the assertion.
45
     *
46
     * If the NameId is null, no subject was included in the assertion.
47
     *
48
     * @var array|null
49
     */
50
    private $nameId;
51
52
    /**
53
     * The encrypted NameId of the subject.
54
     *
55
     * If this is not null, the NameId needs decryption before it can be accessed.
56
     *
57
     * @var \DOMElement|null
58
     */
59
    private $encryptedNameId;
60
61
    /**
62
     * The encrypted Attributes.
63
     *
64
     * If this is not null, these Attributes need decryption before they can be accessed.
65
     *
66
     * @var \DOMElement[]|null
67
     */
68
    private $encryptedAttributes;
69
70
    /**
71
     * Private key we should use to encrypt the attributes.
72
     *
73
     * @var XMLSecurityKey|null
74
     */
75
    private $encryptionKey;
76
77
     /**
78
     * The earliest time this assertion is valid, as an UNIX timestamp.
79
     *
80
     * @var int
81
     */
82
    private $notBefore;
83
84
    /**
85
     * The time this assertion expires, as an UNIX timestamp.
86
     *
87
     * @var int
88
     */
89
    private $notOnOrAfter;
90
91
    /**
92
     * The set of audiences that are allowed to receive this assertion.
93
     *
94
     * This is an array of valid service providers.
95
     *
96
     * If no restrictions on the audience are present, this variable contains null.
97
     *
98
     * @var array|null
99
     */
100
    private $validAudiences;
101
102
    /**
103
     * The session expiration timestamp.
104
     *
105
     * @var int|null
106
     */
107
    private $sessionNotOnOrAfter;
108
109
    /**
110
     * The session index for this user on the IdP.
111
     *
112
     * Contains null if no session index is present.
113
     *
114
     * @var string|null
115
     */
116
    private $sessionIndex;
117
118
    /**
119
     * The timestamp the user was authenticated, as an UNIX timestamp.
120
     *
121
     * @var int
122
     */
123
    private $authnInstant;
124
125
    /**
126
     * The authentication context reference for this assertion.
127
     *
128
     * @var string|null
129
     */
130
    private $authnContextClassRef;
131
132
    /**
133
     * Authentication context declaration provided by value.
134
     *
135
     * See:
136
     * @url http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
137
     *
138
     * @var \SAML2\XML\Chunk
139
     */
140
    private $authnContextDecl;
141
142
    /**
143
     * URI reference that identifies an authentication context declaration.
144
     *
145
     * The URI reference MAY directly resolve into an XML document containing the referenced declaration.
146
     *
147
     * @var \SAML2\XML\Chunk
148
     */
149
    private $authnContextDeclRef;
150
151
    /**
152
     * The list of AuthenticatingAuthorities for this assertion.
153
     *
154
     * @var array
155
     */
156
    private $AuthenticatingAuthority;
157
158
    /**
159
     * The attributes, as an associative array, indexed by attribute name
160
     *
161
     * To ease handling, all attribute values are represented as an array of values, also for values with a multiplicity
162
     * of single. There are 5 possible variants of datatypes for the values: a string, an integer, an array, a
163
     * DOMNodeList or a SAML2\XML\saml\NameID object.
164
     *
165
     * If the attribute is an eduPersonTargetedID, the values will be SAML2\XML\saml\NameID objects.
166
     * If the attribute value has an type-definition (xsi:string or xsi:int), the values will be of that type.
167
     * If the attribute value contains a nested XML structure, the values will be a DOMNodeList
168
     * In all other cases the values are treated as strings
169
     *
170
     * **WARNING** a DOMNodeList cannot be serialized without data-loss and should be handled explicitly
171
     *
172
     * @var array multi-dimensional array of \DOMNodeList|\SAML2\XML\saml\NameID|string|int|array
173
     */
174
    private $attributes;
175
176
    /**
177
     * The NameFormat used on all attributes.
178
     *
179
     * If more than one NameFormat is used, this will contain
180
     * the unspecified nameformat.
181
     *
182
     * @var string
183
     */
184
    private $nameFormat;
185
186
    /**
187
     * The private key we should use to sign the assertion.
188
     *
189
     * The private key can be null, in which case the assertion is sent unsigned.
190
     *
191
     * @var XMLSecurityKey|null
192
     */
193
    private $signatureKey;
194
195
    /**
196
     * List of certificates that should be included in the assertion.
197
     *
198
     * @var array
199
     */
200
    private $certificates;
201
202
    /**
203
     * The data needed to verify the signature.
204
     *
205
     * @var array|null
206
     */
207
    private $signatureData;
208
209
    /**
210
     * Boolean that indicates if attributes are encrypted in the
211
     * assertion or not.
212
     *
213
     * @var boolean
214
     */
215
    private $requiredEncAttributes;
216
217
    /**
218
     * The SubjectConfirmation elements of the Subject in the assertion.
219
     *
220
     * @var \SAML2\XML\saml\SubjectConfirmation[].
221
     */
222
    private $SubjectConfirmation;
223
224
    /**
225
     * @var bool
226
     */
227
    protected $wasSignedAtConstruction = false;
228
229
    /**
230
     * @var string|null
231
     */
232
    private $signatureMethod;
233
234
    /**
235
     * Constructor for SAML 2 assertions.
236
     *
237
     * @param \DOMElement|null $xml The input assertion.
238
     * @throws \Exception
239
     */
240
    public function __construct(\DOMElement $xml = null)
241
    {
242
        $this->id = Utils::getContainer()->generateId();
243
        $this->issueInstant = Temporal::getTime();
244
        $this->issuer = '';
245
        $this->authnInstant = Temporal::getTime();
246
        $this->attributes = array();
247
        $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
248
        $this->certificates = array();
249
        $this->AuthenticatingAuthority = array();
250
        $this->SubjectConfirmation = array();
251
252
        if ($xml === null) {
253
            return;
254
        }
255
256
        if (!$xml->hasAttribute('ID')) {
257
            throw new \Exception('Missing ID attribute on SAML assertion.');
258
        }
259
        $this->id = $xml->getAttribute('ID');
260
261
        if ($xml->getAttribute('Version') !== '2.0') {
262
            /* Currently a very strict check. */
263
            throw new \Exception('Unsupported version: ' . $xml->getAttribute('Version'));
264
        }
265
266
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
267
268
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
269
        if (empty($issuer)) {
270
            throw new \Exception('Missing <saml:Issuer> in assertion.');
271
        }
272
        $this->issuer = new XML\saml\Issuer($issuer[0]);
273
        if ($this->issuer->Format === Constants::NAMEID_ENTITY) {
274
            $this->issuer = $this->issuer->value;
275
        }
276
277
        $this->parseSubject($xml);
278
        $this->parseConditions($xml);
279
        $this->parseAuthnStatement($xml);
280
        $this->parseAttributes($xml);
281
        $this->parseEncryptedAttributes($xml);
282
        $this->parseSignature($xml);
283
    }
284
285
    /**
286
     * Parse subject in assertion.
287
     *
288
     * @param \DOMElement $xml The assertion XML element.
289
     * @throws \Exception
290
     */
291
    private function parseSubject(\DOMElement $xml)
292
    {
293
        $subject = Utils::xpQuery($xml, './saml_assertion:Subject');
294
        if (empty($subject)) {
295
            /* No Subject node. */
296
297
            return;
298
        } elseif (count($subject) > 1) {
299
            throw new \Exception('More than one <saml:Subject> in <saml:Assertion>.');
300
        }
301
        $subject = $subject[0];
302
303
        $nameId = Utils::xpQuery(
304
            $subject,
305
            './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData'
306
        );
307
        if (count($nameId) > 1) {
308
            throw new \Exception('More than one <saml:NameID> or <saml:EncryptedID> in <saml:Subject>.');
309
        } elseif (!empty($nameId)) {
310
            $nameId = $nameId[0];
311
            if ($nameId->localName === 'EncryptedData') {
312
                /* The NameID element is encrypted. */
313
                $this->encryptedNameId = $nameId;
314
            } else {
315
                $this->nameId = Utils::parseNameId($nameId);
316
            }
317
        }
318
319
        $subjectConfirmation = Utils::xpQuery($subject, './saml_assertion:SubjectConfirmation');
320
        if (empty($subjectConfirmation) && empty($nameId)) {
321
            throw new \Exception('Missing <saml:SubjectConfirmation> in <saml:Subject>.');
322
        }
323
324
        foreach ($subjectConfirmation as $sc) {
325
            $this->SubjectConfirmation[] = new SubjectConfirmation($sc);
326
        }
327
    }
328
329
    /**
330
     * Parse conditions in assertion.
331
     *
332
     * @param \DOMElement $xml The assertion XML element.
333
     * @throws \Exception
334
     */
335
    private function parseConditions(\DOMElement $xml)
336
    {
337
        $conditions = Utils::xpQuery($xml, './saml_assertion:Conditions');
338
        if (empty($conditions)) {
339
            /* No <saml:Conditions> node. */
340
341
            return;
342
        } elseif (count($conditions) > 1) {
343
            throw new \Exception('More than one <saml:Conditions> in <saml:Assertion>.');
344
        }
345
        $conditions = $conditions[0];
346
347 View Code Duplication
        if ($conditions->hasAttribute('NotBefore')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
            $notBefore = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotBefore'));
349
            if ($this->notBefore === null || $this->notBefore < $notBefore) {
350
                $this->notBefore = $notBefore;
351
            }
352
        }
353 View Code Duplication
        if ($conditions->hasAttribute('NotOnOrAfter')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
354
            $notOnOrAfter = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotOnOrAfter'));
355
            if ($this->notOnOrAfter === null || $this->notOnOrAfter > $notOnOrAfter) {
356
                $this->notOnOrAfter = $notOnOrAfter;
357
            }
358
        }
359
360
        for ($node = $conditions->firstChild; $node !== null; $node = $node->nextSibling) {
361
            if ($node instanceof \DOMText) {
362
                continue;
363
            }
364 View Code Duplication
            if ($node->namespaceURI !== Constants::NS_SAML) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
                throw new \Exception('Unknown namespace of condition: ' . var_export($node->namespaceURI, true));
366
            }
367
            switch ($node->localName) {
368
                case 'AudienceRestriction':
369
                    $audiences = Utils::extractStrings($node, Constants::NS_SAML, 'Audience');
0 ignored issues
show
Compatibility introduced by
$node of type object<DOMNode> is not a sub-type of object<DOMElement>. It seems like you assume a child class of the class DOMNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
370
                    if ($this->validAudiences === null) {
371
                        /* The first (and probably last) AudienceRestriction element. */
372
                        $this->validAudiences = $audiences;
373
                    } else {
374
                        /*
375
                         * The set of AudienceRestriction are ANDed together, so we need
376
                         * the subset that are present in all of them.
377
                         */
378
                        $this->validAudiences = array_intersect($this->validAudiences, $audiences);
379
                    }
380
                    break;
381
                case 'OneTimeUse':
382
                    /* Currently ignored. */
383
                    break;
384
                case 'ProxyRestriction':
385
                    /* Currently ignored. */
386
                    break;
387
                default:
388
                    throw new \Exception('Unknown condition: ' . var_export($node->localName, true));
389
            }
390
        }
391
    }
392
393
    /**
394
     * Parse AuthnStatement in assertion.
395
     *
396
     * @param \DOMElement $xml The assertion XML element.
397
     * @throws \Exception
398
     */
399
    private function parseAuthnStatement(\DOMElement $xml)
400
    {
401
        $authnStatements = Utils::xpQuery($xml, './saml_assertion:AuthnStatement');
402
        if (empty($authnStatements)) {
403
            $this->authnInstant = null;
404
405
            return;
406
        } elseif (count($authnStatements) > 1) {
407
            throw new \Exception('More than one <saml:AuthnStatement> in <saml:Assertion> not supported.');
408
        }
409
        $authnStatement = $authnStatements[0];
410
411
        if (!$authnStatement->hasAttribute('AuthnInstant')) {
412
            throw new \Exception('Missing required AuthnInstant attribute on <saml:AuthnStatement>.');
413
        }
414
        $this->authnInstant = Utils::xsDateTimeToTimestamp($authnStatement->getAttribute('AuthnInstant'));
415
416
        if ($authnStatement->hasAttribute('SessionNotOnOrAfter')) {
417
            $this->sessionNotOnOrAfter = Utils::xsDateTimeToTimestamp($authnStatement->getAttribute('SessionNotOnOrAfter'));
418
        }
419
420
        if ($authnStatement->hasAttribute('SessionIndex')) {
421
            $this->sessionIndex = $authnStatement->getAttribute('SessionIndex');
422
        }
423
424
        $this->parseAuthnContext($authnStatement);
425
    }
426
427
    /**
428
     * Parse AuthnContext in AuthnStatement.
429
     *
430
     * @param \DOMElement $authnStatementEl
431
     * @throws \Exception
432
     */
433
    private function parseAuthnContext(\DOMElement $authnStatementEl)
434
    {
435
        // Get the AuthnContext element
436
        $authnContexts = Utils::xpQuery($authnStatementEl, './saml_assertion:AuthnContext');
437
        if (count($authnContexts) > 1) {
438
            throw new \Exception('More than one <saml:AuthnContext> in <saml:AuthnStatement>.');
439
        } elseif (empty($authnContexts)) {
440
            throw new \Exception('Missing required <saml:AuthnContext> in <saml:AuthnStatement>.');
441
        }
442
        $authnContextEl = $authnContexts[0];
443
444
        // Get the AuthnContextDeclRef (if available)
445
        $authnContextDeclRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDeclRef');
446
        if (count($authnContextDeclRefs) > 1) {
447
            throw new \Exception(
448
                'More than one <saml:AuthnContextDeclRef> found?'
449
            );
450
        } elseif (count($authnContextDeclRefs) === 1) {
451
            $this->setAuthnContextDeclRef(trim($authnContextDeclRefs[0]->textContent));
452
        }
453
454
        // Get the AuthnContextDecl (if available)
455
        $authnContextDecls = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDecl');
456
        if (count($authnContextDecls) > 1) {
457
            throw new \Exception(
458
                'More than one <saml:AuthnContextDecl> found?'
459
            );
460
        } elseif (count($authnContextDecls) === 1) {
461
            $this->setAuthnContextDecl(new Chunk($authnContextDecls[0]));
462
        }
463
464
        // Get the AuthnContextClassRef (if available)
465
        $authnContextClassRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextClassRef');
466
        if (count($authnContextClassRefs) > 1) {
467
            throw new \Exception('More than one <saml:AuthnContextClassRef> in <saml:AuthnContext>.');
468
        } elseif (count($authnContextClassRefs) === 1) {
469
            $this->setAuthnContextClassRef(trim($authnContextClassRefs[0]->textContent));
470
        }
471
472
        // Constraint from XSD: MUST have one of the three
473
        if (empty($this->authnContextClassRef) && empty($this->authnContextDecl) && empty($this->authnContextDeclRef)) {
474
            throw new \Exception(
475
                'Missing either <saml:AuthnContextClassRef> or <saml:AuthnContextDeclRef> or <saml:AuthnContextDecl>'
476
            );
477
        }
478
479
        $this->AuthenticatingAuthority = Utils::extractStrings(
480
            $authnContextEl,
481
            Constants::NS_SAML,
482
            'AuthenticatingAuthority'
483
        );
484
    }
485
486
    /**
487
     * Parse attribute statements in assertion.
488
     *
489
     * @param \DOMElement $xml The XML element with the assertion.
490
     * @throws \Exception
491
     */
492
    private function parseAttributes(\DOMElement $xml)
493
    {
494
        $firstAttribute = true;
495
        $attributes = Utils::xpQuery($xml, './saml_assertion:AttributeStatement/saml_assertion:Attribute');
496
        foreach ($attributes as $attribute) {
497
            if (!$attribute->hasAttribute('Name')) {
498
                throw new \Exception('Missing name on <saml:Attribute> element.');
499
            }
500
            $name = $attribute->getAttribute('Name');
501
502
            if ($attribute->hasAttribute('NameFormat')) {
503
                $nameFormat = $attribute->getAttribute('NameFormat');
504
            } else {
505
                $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
506
            }
507
508
            if ($firstAttribute) {
509
                $this->nameFormat = $nameFormat;
510
                $firstAttribute = false;
511
            } else {
512
                if ($this->nameFormat !== $nameFormat) {
513
                    $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
514
                }
515
            }
516
517
            if (!array_key_exists($name, $this->attributes)) {
518
                $this->attributes[$name] = array();
519
            }
520
521
            $this->parseAttributeValue($attribute, $name);
522
        }
523
    }
524
525
    /**
526
     * @param \DOMNode $attribute
527
     * @param string   $attributeName
528
     */
529
    private function parseAttributeValue($attribute, $attributeName)
530
    {
531
        /** @var \DOMElement[] $values */
532
        $values = Utils::xpQuery($attribute, './saml_assertion:AttributeValue');
533
534
        if ($attributeName === Constants::EPTI_URN_MACE || $attributeName === Constants::EPTI_URN_OID) {
535
            foreach ($values as $index => $eptiAttributeValue) {
536
                $eptiNameId = Utils::xpQuery($eptiAttributeValue, './saml:NameID');
537
538
                if (count($eptiNameId) !== 1) {
539
                    throw new RuntimeException(sprintf(
540
                        'A "%s" (EPTI) attribute value must be a NameID, none found for value no. "%d"',
541
                        $attributeName,
542
                        $index
543
                    ));
544
                }
545
546
                $this->attributes[$attributeName][] = new XML\saml\NameID($eptiNameId[0]);
547
            }
548
549
            return;
550
        }
551
552
        foreach ($values as $value) {
553
            $hasNonTextChildElements = false;
554
            foreach ($value->childNodes as $childNode) {
555
                /** @var \DOMNode $childNode */
556
                if ($childNode->nodeType !== XML_TEXT_NODE) {
557
                    $hasNonTextChildElements = true;
558
                    break;
559
                }
560
            }
561
562
            if ($hasNonTextChildElements) {
563
                $this->attributes[$attributeName][] = $value->childNodes;
564
                continue;
565
            }
566
567
            $type = $value->getAttribute('xsi:type');
568
            if ($type === 'xs:integer') {
569
                $this->attributes[$attributeName][] = (int)$value->textContent;
570
            } else {
571
                $this->attributes[$attributeName][] = trim($value->textContent);
572
            }
573
        }
574
    }
575
576
    /**
577
     * Parse encrypted attribute statements in assertion.
578
     *
579
     * @param \DOMElement $xml The XML element with the assertion.
580
     */
581
    private function parseEncryptedAttributes(\DOMElement $xml)
582
    {
583
        $this->encryptedAttributes = Utils::xpQuery(
584
            $xml,
585
            './saml_assertion:AttributeStatement/saml_assertion:EncryptedAttribute'
586
        );
587
    }
588
589
    /**
590
     * Parse signature on assertion.
591
     *
592
     * @param \DOMElement $xml The assertion XML element.
593
     */
594
    private function parseSignature(\DOMElement $xml)
595
    {
596
        /** @var null|\DOMAttr $signatureMethod */
597
        $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
598
599
        /* Validate the signature element of the message. */
600
        $sig = Utils::validateElement($xml);
601
        if ($sig !== false) {
602
            $this->wasSignedAtConstruction = true;
603
            $this->certificates = $sig['Certificates'];
604
            $this->signatureData = $sig;
605
            $this->signatureMethod = $signatureMethod[0]->value;
606
        }
607
    }
608
609
    /**
610
     * Validate this assertion against a public key.
611
     *
612
     * If no signature was present on the assertion, we will return false.
613
     * Otherwise, true will be returned. An exception is thrown if the
614
     * signature validation fails.
615
     *
616
     * @param  XMLSecurityKey $key The key we should check against.
617
     * @return boolean        true if successful, false if it is unsigned.
618
     */
619
    public function validate(XMLSecurityKey $key)
620
    {
621
        assert('$key->type === \RobRichards\XMLSecLibs\XMLSecurityKey::RSA_SHA1');
622
623
        if ($this->signatureData === null) {
624
            return false;
625
        }
626
627
        Utils::validateSignature($this->signatureData, $key);
628
629
        return true;
630
    }
631
632
    /**
633
     * Retrieve the identifier of this assertion.
634
     *
635
     * @return string The identifier of this assertion.
636
     */
637
    public function getId()
638
    {
639
        return $this->id;
640
    }
641
642
    /**
643
     * Set the identifier of this assertion.
644
     *
645
     * @param string $id The new identifier of this assertion.
646
     */
647
    public function setId($id)
648
    {
649
        assert('is_string($id)');
650
651
        $this->id = $id;
652
    }
653
654
    /**
655
     * Retrieve the issue timestamp of this assertion.
656
     *
657
     * @return int The issue timestamp of this assertion, as an UNIX timestamp.
658
     */
659
    public function getIssueInstant()
660
    {
661
        return $this->issueInstant;
662
    }
663
664
    /**
665
     * Set the issue timestamp of this assertion.
666
     *
667
     * @param int $issueInstant The new issue timestamp of this assertion, as an UNIX timestamp.
668
     */
669
    public function setIssueInstant($issueInstant)
670
    {
671
        assert('is_int($issueInstant)');
672
673
        $this->issueInstant = $issueInstant;
674
    }
675
676
    /**
677
     * Retrieve the issuer if this assertion.
678
     *
679
     * @return string|\SAML2\XML\saml\Issuer The issuer of this assertion.
680
     */
681
    public function getIssuer()
682
    {
683
        return $this->issuer;
684
    }
685
686
    /**
687
     * Set the issuer of this message.
688
     *
689
     * @param string|\SAML2\XML\saml\Issuer $issuer The new issuer of this assertion.
690
     */
691
    public function setIssuer($issuer)
692
    {
693
        assert('is_string($issuer) || $issuer instanceof \SAML2\XML\saml\Issuer');
694
695
        $this->issuer = $issuer;
696
    }
697
698
    /**
699
     * Retrieve the NameId of the subject in the assertion.
700
     *
701
     * The returned NameId is in the format used by \SAML2\Utils::addNameId().
702
     *
703
     * @see \SAML2\Utils::addNameId()
704
     * @return array|null The name identifier of the assertion.
705
     * @throws \Exception
706
     */
707
    public function getNameId()
708
    {
709
        if ($this->encryptedNameId !== null) {
710
            throw new \Exception('Attempted to retrieve encrypted NameID without decrypting it first.');
711
        }
712
713
        return $this->nameId;
714
    }
715
716
    /**
717
     * Set the NameId of the subject in the assertion.
718
     *
719
     * The NameId must be in the format accepted by \SAML2\Utils::addNameId().
720
     *
721
     * @see \SAML2\Utils::addNameId()
722
     * @param array|null $nameId The name identifier of the assertion.
723
     */
724
    public function setNameId($nameId)
725
    {
726
        assert('is_array($nameId) || is_null($nameId)');
727
728
        $this->nameId = $nameId;
729
    }
730
731
    /**
732
     * Check whether the NameId is encrypted.
733
     *
734
     * @return true if the NameId is encrypted, false if not.
735
     */
736
    public function isNameIdEncrypted()
737
    {
738
        return $this->encryptedNameId !== null;
739
    }
740
741
    /**
742
     * Encrypt the NameID in the Assertion.
743
     *
744
     * @param XMLSecurityKey $key The encryption key.
745
     */
746 View Code Duplication
    public function encryptNameId(XMLSecurityKey $key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
747
    {
748
        /* First create a XML representation of the NameID. */
749
        $doc = DOMDocumentFactory::create();
750
        $root = $doc->createElement('root');
751
        $doc->appendChild($root);
752
        Utils::addNameId($root, $this->nameId);
0 ignored issues
show
Bug introduced by
It seems like $this->nameId can also be of type null; however, SAML2\Utils::addNameId() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
753
        $nameId = $root->firstChild;
754
755
        Utils::getContainer()->debugMessage($nameId, 'encrypt');
0 ignored issues
show
Documentation introduced by
$nameId is of type object<DOMNode>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
756
757
        /* Encrypt the NameID. */
758
        $enc = new XMLSecEnc();
759
        $enc->setNode($nameId);
760
        // @codingStandardsIgnoreStart
761
        $enc->type = XMLSecEnc::Element;
762
        // @codingStandardsIgnoreEnd
763
764
        $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
765
        $symmetricKey->generateSessionKey();
766
        $enc->encryptKey($key, $symmetricKey);
767
768
        $this->encryptedNameId = $enc->encryptNode($symmetricKey);
0 ignored issues
show
Documentation Bug introduced by
It seems like $enc->encryptNode($symmetricKey) can also be of type object<DOMNode>. However, the property $encryptedNameId is declared as type object<DOMElement>|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
769
        $this->nameId = null;
770
    }
771
772
    /**
773
     * Decrypt the NameId of the subject in the assertion.
774
     *
775
     * @param XMLSecurityKey $key       The decryption key.
776
     * @param array          $blacklist Blacklisted decryption algorithms.
777
     */
778 View Code Duplication
    public function decryptNameId(XMLSecurityKey $key, array $blacklist = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
779
    {
780
        if ($this->encryptedNameId === null) {
781
            /* No NameID to decrypt. */
782
783
            return;
784
        }
785
786
        $nameId = Utils::decryptElement($this->encryptedNameId, $key, $blacklist);
787
        Utils::getContainer()->debugMessage($nameId, 'decrypt');
0 ignored issues
show
Documentation introduced by
$nameId is of type object<DOMElement>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
788
        $this->nameId = Utils::parseNameId($nameId);
789
790
        $this->encryptedNameId = null;
791
    }
792
793
    /**
794
     * Did this Assertion contain encrypted Attributes?
795
     *
796
     * @return bool
797
     */
798
    public function hasEncryptedAttributes()
799
    {
800
        return $this->encryptedAttributes !== null;
801
    }
802
803
    /**
804
     * Decrypt the assertion attributes.
805
     *
806
     * @param XMLSecurityKey $key
807
     * @param array $blacklist
808
     * @throws \Exception
809
     */
810
    public function decryptAttributes(XMLSecurityKey $key, array $blacklist = array())
811
    {
812
        if ($this->encryptedAttributes === null) {
813
            return;
814
        }
815
        $firstAttribute = true;
816
        $attributes = $this->encryptedAttributes;
817
        foreach ($attributes as $attributeEnc) {
818
            /*Decrypt node <EncryptedAttribute>*/
819
            $attribute = Utils::decryptElement(
820
                $attributeEnc->getElementsByTagName('EncryptedData')->item(0),
0 ignored issues
show
Compatibility introduced by
$attributeEnc->getElemen...ncryptedData')->item(0) of type object<DOMNode> is not a sub-type of object<DOMElement>. It seems like you assume a child class of the class DOMNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
821
                $key,
822
                $blacklist
823
            );
824
825
            if (!$attribute->hasAttribute('Name')) {
826
                throw new \Exception('Missing name on <saml:Attribute> element.');
827
            }
828
            $name = $attribute->getAttribute('Name');
829
830
            if ($attribute->hasAttribute('NameFormat')) {
831
                $nameFormat = $attribute->getAttribute('NameFormat');
832
            } else {
833
                $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
834
            }
835
836
            if ($firstAttribute) {
837
                $this->nameFormat = $nameFormat;
838
                $firstAttribute = false;
839
            } else {
840
                if ($this->nameFormat !== $nameFormat) {
841
                    $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
842
                }
843
            }
844
845
            if (!array_key_exists($name, $this->attributes)) {
846
                $this->attributes[$name] = array();
847
            }
848
849
            $this->parseAttributeValue($attribute, $name);
850
        }
851
    }
852
853
    /**
854
     * Retrieve the earliest timestamp this assertion is valid.
855
     *
856
     * This function returns null if there are no restrictions on how early the
857
     * assertion can be used.
858
     *
859
     * @return int|null The earliest timestamp this assertion is valid.
860
     */
861
    public function getNotBefore()
862
    {
863
        return $this->notBefore;
864
    }
865
866
    /**
867
     * Set the earliest timestamp this assertion can be used.
868
     *
869
     * Set this to null if no limit is required.
870
     *
871
     * @param int|null $notBefore The earliest timestamp this assertion is valid.
872
     */
873
    public function setNotBefore($notBefore)
874
    {
875
        assert('is_int($notBefore) || is_null($notBefore)');
876
877
        $this->notBefore = $notBefore;
878
    }
879
880
    /**
881
     * Retrieve the expiration timestamp of this assertion.
882
     *
883
     * This function returns null if there are no restrictions on how
884
     * late the assertion can be used.
885
     *
886
     * @return int|null The latest timestamp this assertion is valid.
887
     */
888
    public function getNotOnOrAfter()
889
    {
890
        return $this->notOnOrAfter;
891
    }
892
893
    /**
894
     * Set the expiration timestamp of this assertion.
895
     *
896
     * Set this to null if no limit is required.
897
     *
898
     * @param int|null $notOnOrAfter The latest timestamp this assertion is valid.
899
     */
900
    public function setNotOnOrAfter($notOnOrAfter)
901
    {
902
        assert('is_int($notOnOrAfter) || is_null($notOnOrAfter)');
903
904
        $this->notOnOrAfter = $notOnOrAfter;
905
    }
906
907
    /**
908
     * Set $EncryptedAttributes if attributes will send encrypted
909
     *
910
     * @param boolean $ea true to encrypt attributes in the assertion.
911
     */
912
    public function setEncryptedAttributes($ea)
913
    {
914
        $this->requiredEncAttributes = $ea;
915
    }
916
917
    /**
918
     * Retrieve the audiences that are allowed to receive this assertion.
919
     *
920
     * This may be null, in which case all audiences are allowed.
921
     *
922
     * @return array|null The allowed audiences.
923
     */
924
    public function getValidAudiences()
925
    {
926
        return $this->validAudiences;
927
    }
928
929
    /**
930
     * Set the audiences that are allowed to receive this assertion.
931
     *
932
     * This may be null, in which case all audiences are allowed.
933
     *
934
     * @param array|null $validAudiences The allowed audiences.
935
     */
936
    public function setValidAudiences(array $validAudiences = null)
937
    {
938
        $this->validAudiences = $validAudiences;
939
    }
940
941
    /**
942
     * Retrieve the AuthnInstant of the assertion.
943
     *
944
     * @return int|null The timestamp the user was authenticated, or NULL if the user isn't authenticated.
945
     */
946
    public function getAuthnInstant()
947
    {
948
        return $this->authnInstant;
949
    }
950
951
952
    /**
953
     * Set the AuthnInstant of the assertion.
954
     *
955
     * @param int|null $authnInstant Timestamp the user was authenticated, or NULL if we don't want an AuthnStatement.
956
     */
957
    public function setAuthnInstant($authnInstant)
958
    {
959
        assert('is_int($authnInstant) || is_null($authnInstant)');
960
961
        $this->authnInstant = $authnInstant;
962
    }
963
964
    /**
965
     * Retrieve the session expiration timestamp.
966
     *
967
     * This function returns null if there are no restrictions on the
968
     * session lifetime.
969
     *
970
     * @return int|null The latest timestamp this session is valid.
971
     */
972
    public function getSessionNotOnOrAfter()
973
    {
974
        return $this->sessionNotOnOrAfter;
975
    }
976
977
    /**
978
     * Set the session expiration timestamp.
979
     *
980
     * Set this to null if no limit is required.
981
     *
982
     * @param int|null $sessionNotOnOrAfter The latest timestamp this session is valid.
983
     */
984
    public function setSessionNotOnOrAfter($sessionNotOnOrAfter)
985
    {
986
        assert('is_int($sessionNotOnOrAfter) || is_null($sessionNotOnOrAfter)');
987
988
        $this->sessionNotOnOrAfter = $sessionNotOnOrAfter;
989
    }
990
991
    /**
992
     * Retrieve the session index of the user at the IdP.
993
     *
994
     * @return string|null The session index of the user at the IdP.
995
     */
996
    public function getSessionIndex()
997
    {
998
        return $this->sessionIndex;
999
    }
1000
1001
    /**
1002
     * Set the session index of the user at the IdP.
1003
     *
1004
     * Note that the authentication context must be set before the
1005
     * session index can be inluded in the assertion.
1006
     *
1007
     * @param string|null $sessionIndex The session index of the user at the IdP.
1008
     */
1009
    public function setSessionIndex($sessionIndex)
1010
    {
1011
        assert('is_string($sessionIndex) || is_null($sessionIndex)');
1012
1013
        $this->sessionIndex = $sessionIndex;
1014
    }
1015
1016
    /**
1017
     * Retrieve the authentication method used to authenticate the user.
1018
     *
1019
     * This will return null if no authentication statement was
1020
     * included in the assertion.
1021
     *
1022
     * Note that this returns either the AuthnContextClassRef or the AuthnConextDeclRef, whose definition overlaps
1023
     * but is slightly different (consult the specification for more information).
1024
     * This was done to work around an old bug of Shibboleth ( https://bugs.internet2.edu/jira/browse/SIDP-187 ).
1025
     * Should no longer be required, please use either getAuthnConextClassRef or getAuthnContextDeclRef.
1026
     *
1027
     * @deprecated use getAuthnContextClassRef
1028
     * @return string|null The authentication method.
1029
     */
1030
    public function getAuthnContext()
1031
    {
1032
        if (!empty($this->authnContextClassRef)) {
1033
            return $this->authnContextClassRef;
1034
        }
1035
        if (!empty($this->authnContextDeclRef)) {
1036
            return $this->authnContextDeclRef;
1037
        }
1038
        return null;
1039
    }
1040
1041
    /**
1042
     * Set the authentication method used to authenticate the user.
1043
     *
1044
     * If this is set to null, no authentication statement will be
1045
     * included in the assertion. The default is null.
1046
     *
1047
     * @deprecated use setAuthnContextClassRef
1048
     * @param string|null $authnContext The authentication method.
1049
     */
1050
    public function setAuthnContext($authnContext)
1051
    {
1052
        $this->setAuthnContextClassRef($authnContext);
1053
    }
1054
1055
    /**
1056
     * Retrieve the authentication method used to authenticate the user.
1057
     *
1058
     * This will return null if no authentication statement was
1059
     * included in the assertion.
1060
     *
1061
     * @return string|null The authentication method.
1062
     */
1063
    public function getAuthnContextClassRef()
1064
    {
1065
        return $this->authnContextClassRef;
1066
    }
1067
1068
    /**
1069
     * Set the authentication method used to authenticate the user.
1070
     *
1071
     * If this is set to null, no authentication statement will be
1072
     * included in the assertion. The default is null.
1073
     *
1074
     * @param string|null $authnContextClassRef The authentication method.
1075
     */
1076
    public function setAuthnContextClassRef($authnContextClassRef)
1077
    {
1078
        assert('is_string($authnContextClassRef) || is_null($authnContextClassRef)');
1079
1080
        $this->authnContextClassRef = $authnContextClassRef;
1081
    }
1082
1083
    /**
1084
     * Set the authentication context declaration.
1085
     *
1086
     * @param \SAML2\XML\Chunk $authnContextDecl
1087
     * @throws \Exception
1088
     */
1089
    public function setAuthnContextDecl(Chunk $authnContextDecl)
1090
    {
1091
        if (!empty($this->authnContextDeclRef)) {
1092
            throw new \Exception(
1093
                'AuthnContextDeclRef is already registered! May only have either a Decl or a DeclRef, not both!'
1094
            );
1095
        }
1096
1097
        $this->authnContextDecl = $authnContextDecl;
1098
    }
1099
1100
    /**
1101
     * Get the authentication context declaration.
1102
     *
1103
     * See:
1104
     * @url http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
1105
     *
1106
     * @return \SAML2\XML\Chunk|null
1107
     */
1108
    public function getAuthnContextDecl()
1109
    {
1110
        return $this->authnContextDecl;
1111
    }
1112
1113
    /**
1114
     * Set the authentication context declaration reference.
1115
     *
1116
     * @param string $authnContextDeclRef
1117
     * @throws \Exception
1118
     */
1119
    public function setAuthnContextDeclRef($authnContextDeclRef)
1120
    {
1121
        if (!empty($this->authnContextDecl)) {
1122
            throw new \Exception(
1123
                'AuthnContextDecl is already registered! May only have either a Decl or a DeclRef, not both!'
1124
            );
1125
        }
1126
1127
        $this->authnContextDeclRef = $authnContextDeclRef;
0 ignored issues
show
Documentation Bug introduced by
It seems like $authnContextDeclRef of type string is incompatible with the declared type object<SAML2\XML\Chunk> of property $authnContextDeclRef.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1128
    }
1129
1130
    /**
1131
     * Get the authentication context declaration reference.
1132
     * URI reference that identifies an authentication context declaration.
1133
     *
1134
     * The URI reference MAY directly resolve into an XML document containing the referenced declaration.
1135
     *
1136
     * @return string
1137
     */
1138
    public function getAuthnContextDeclRef()
1139
    {
1140
        return $this->authnContextDeclRef;
1141
    }
1142
1143
    /**
1144
     * Retrieve the AuthenticatingAuthority.
1145
     *
1146
     *
1147
     * @return array
1148
     */
1149
    public function getAuthenticatingAuthority()
1150
    {
1151
        return $this->AuthenticatingAuthority;
1152
    }
1153
1154
    /**
1155
     * Set the AuthenticatingAuthority
1156
     *
1157
     *
1158
     * @param array.
1159
     */
1160
    public function setAuthenticatingAuthority($authenticatingAuthority)
1161
    {
1162
        $this->AuthenticatingAuthority = $authenticatingAuthority;
1163
    }
1164
1165
    /**
1166
     * Retrieve all attributes.
1167
     *
1168
     * @return array All attributes, as an associative array.
1169
     */
1170
    public function getAttributes()
1171
    {
1172
        return $this->attributes;
1173
    }
1174
1175
    /**
1176
     * Replace all attributes.
1177
     *
1178
     * @param array $attributes All new attributes, as an associative array.
1179
     */
1180
    public function setAttributes(array $attributes)
1181
    {
1182
        $this->attributes = $attributes;
1183
    }
1184
1185
    /**
1186
     * Retrieve the NameFormat used on all attributes.
1187
     *
1188
     * If more than one NameFormat is used in the received attributes, this
1189
     * returns the unspecified NameFormat.
1190
     *
1191
     * @return string The NameFormat used on all attributes.
1192
     */
1193
    public function getAttributeNameFormat()
1194
    {
1195
        return $this->nameFormat;
1196
    }
1197
1198
    /**
1199
     * Set the NameFormat used on all attributes.
1200
     *
1201
     * @param string $nameFormat The NameFormat used on all attributes.
1202
     */
1203
    public function setAttributeNameFormat($nameFormat)
1204
    {
1205
        assert('is_string($nameFormat)');
1206
1207
        $this->nameFormat = $nameFormat;
1208
    }
1209
1210
    /**
1211
     * Retrieve the SubjectConfirmation elements we have in our Subject element.
1212
     *
1213
     * @return array Array of \SAML2\XML\saml\SubjectConfirmation elements.
1214
     */
1215
    public function getSubjectConfirmation()
1216
    {
1217
        return $this->SubjectConfirmation;
1218
    }
1219
1220
    /**
1221
     * Set the SubjectConfirmation elements that should be included in the assertion.
1222
     *
1223
     * @param array $SubjectConfirmation Array of \SAML2\XML\saml\SubjectConfirmation elements.
1224
     */
1225
    public function setSubjectConfirmation(array $SubjectConfirmation)
1226
    {
1227
        $this->SubjectConfirmation = $SubjectConfirmation;
1228
    }
1229
1230
    /**
1231
     * Retrieve the private key we should use to sign the assertion.
1232
     *
1233
     * @return XMLSecurityKey|null The key, or NULL if no key is specified.
1234
     */
1235
    public function getSignatureKey()
1236
    {
1237
        return $this->signatureKey;
1238
    }
1239
1240
    /**
1241
     * Set the private key we should use to sign the assertion.
1242
     *
1243
     * If the key is null, the assertion will be sent unsigned.
1244
     *
1245
     * @param XMLSecurityKey|null $signatureKey
1246
     */
1247
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
1248
    {
1249
        $this->signatureKey = $signatureKey;
1250
    }
1251
1252
    /**
1253
     * Return the key we should use to encrypt the assertion.
1254
     *
1255
     * @return XMLSecurityKey|null The key, or NULL if no key is specified..
1256
     *
1257
     */
1258
    public function getEncryptionKey()
1259
    {
1260
        return $this->encryptionKey;
1261
    }
1262
1263
    /**
1264
     * Set the private key we should use to encrypt the attributes.
1265
     *
1266
     * @param XMLSecurityKey|null $Key
1267
     */
1268
    public function setEncryptionKey(XMLSecurityKey $Key = null)
1269
    {
1270
        $this->encryptionKey = $Key;
1271
    }
1272
1273
    /**
1274
     * Set the certificates that should be included in the assertion.
1275
     *
1276
     * The certificates should be strings with the PEM encoded data.
1277
     *
1278
     * @param array $certificates An array of certificates.
1279
     */
1280
    public function setCertificates(array $certificates)
1281
    {
1282
        $this->certificates = $certificates;
1283
    }
1284
1285
    /**
1286
     * Retrieve the certificates that are included in the assertion.
1287
     *
1288
     * @return array An array of certificates.
1289
     */
1290
    public function getCertificates()
1291
    {
1292
        return $this->certificates;
1293
    }
1294
1295
    /**
1296
     * @return bool
1297
     */
1298
    public function getWasSignedAtConstruction()
1299
    {
1300
        return $this->wasSignedAtConstruction;
1301
    }
1302
1303
    /**
1304
     * @return null|string
1305
     */
1306
    public function getSignatureMethod()
1307
    {
1308
        return $this->signatureMethod;
1309
    }
1310
1311
    /**
1312
     * Convert this assertion to an XML element.
1313
     *
1314
     * @param  \DOMNode|null $parentElement The DOM node the assertion should be created in.
1315
     * @return \DOMElement   This assertion.
1316
     */
1317
    public function toXML(\DOMNode $parentElement = null)
1318
    {
1319 View Code Duplication
        if ($parentElement === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1320
            $document = DOMDocumentFactory::create();
1321
            $parentElement = $document;
1322
        } else {
1323
            $document = $parentElement->ownerDocument;
1324
        }
1325
1326
        $root = $document->createElementNS(Constants::NS_SAML, 'saml:' . 'Assertion');
1327
        $parentElement->appendChild($root);
1328
1329
        /* Ugly hack to add another namespace declaration to the root element. */
1330
        $root->setAttributeNS(Constants::NS_SAMLP, 'samlp:tmp', 'tmp');
1331
        $root->removeAttributeNS(Constants::NS_SAMLP, 'tmp');
1332
        $root->setAttributeNS(Constants::NS_XSI, 'xsi:tmp', 'tmp');
1333
        $root->removeAttributeNS(Constants::NS_XSI, 'tmp');
1334
        $root->setAttributeNS(Constants::NS_XS, 'xs:tmp', 'tmp');
1335
        $root->removeAttributeNS(Constants::NS_XS, 'tmp');
1336
1337
        $root->setAttribute('ID', $this->id);
1338
        $root->setAttribute('Version', '2.0');
1339
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
1340
1341 View Code Duplication
        if (is_string($this->issuer)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1342
            $issuer = Utils::addString($root, Constants::NS_SAML, 'saml:Issuer', $this->issuer);
1343
        } elseif ($this->issuer instanceof \SAML2\XML\saml\Issuer) {
1344
            $issuer = $this->issuer->toXML($root);
1345
        }
1346
1347
        $this->addSubject($root);
1348
        $this->addConditions($root);
1349
        $this->addAuthnStatement($root);
1350
        if ($this->requiredEncAttributes == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1351
            $this->addAttributeStatement($root);
1352
        } else {
1353
            $this->addEncryptedAttributeStatement($root);
1354
        }
1355
1356
        if ($this->signatureKey !== null) {
1357
            Utils::insertSignature($this->signatureKey, $this->certificates, $root, $issuer->nextSibling);
0 ignored issues
show
Bug introduced by
The variable $issuer does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1358
        }
1359
1360
        return $root;
1361
    }
1362
1363
    /**
1364
     * Add a Subject-node to the assertion.
1365
     *
1366
     * @param \DOMElement $root The assertion element we should add the subject to.
1367
     */
1368 View Code Duplication
    private function addSubject(\DOMElement $root)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1369
    {
1370
        if ($this->nameId === null && $this->encryptedNameId === null) {
1371
            /* We don't have anything to create a Subject node for. */
1372
1373
            return;
1374
        }
1375
1376
        $subject = $root->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:Subject');
1377
        $root->appendChild($subject);
1378
1379
        if ($this->encryptedNameId === null) {
1380
            Utils::addNameId($subject, $this->nameId);
0 ignored issues
show
Bug introduced by
It seems like $this->nameId can also be of type null; however, SAML2\Utils::addNameId() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1381
        } else {
1382
            $eid = $subject->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:' . 'EncryptedID');
1383
            $subject->appendChild($eid);
1384
            $eid->appendChild($subject->ownerDocument->importNode($this->encryptedNameId, true));
1385
        }
1386
1387
        foreach ($this->SubjectConfirmation as $sc) {
1388
            $sc->toXML($subject);
1389
        }
1390
    }
1391
1392
1393
    /**
1394
     * Add a Conditions-node to the assertion.
1395
     *
1396
     * @param \DOMElement $root The assertion element we should add the conditions to.
1397
     */
1398
    private function addConditions(\DOMElement $root)
1399
    {
1400
        $document = $root->ownerDocument;
1401
1402
        $conditions = $document->createElementNS(Constants::NS_SAML, 'saml:Conditions');
1403
        $root->appendChild($conditions);
1404
1405
        if ($this->notBefore !== null) {
1406
            $conditions->setAttribute('NotBefore', gmdate('Y-m-d\TH:i:s\Z', $this->notBefore));
1407
        }
1408
        if ($this->notOnOrAfter !== null) {
1409
            $conditions->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter));
1410
        }
1411
1412
        if ($this->validAudiences !== null) {
1413
            $ar = $document->createElementNS(Constants::NS_SAML, 'saml:AudienceRestriction');
1414
            $conditions->appendChild($ar);
1415
1416
            Utils::addStrings($ar, Constants::NS_SAML, 'saml:Audience', false, $this->validAudiences);
1417
        }
1418
    }
1419
1420
1421
    /**
1422
     * Add a AuthnStatement-node to the assertion.
1423
     *
1424
     * @param \DOMElement $root The assertion element we should add the authentication statement to.
1425
     */
1426
    private function addAuthnStatement(\DOMElement $root)
1427
    {
1428
        if ($this->authnInstant === null ||
1429
            (
1430
                $this->authnContextClassRef === null &&
1431
                $this->authnContextDecl === null &&
1432
                $this->authnContextDeclRef === null
1433
            )
1434
        ) {
1435
            /* No authentication context or AuthnInstant => no authentication statement. */
1436
1437
            return;
1438
        }
1439
1440
        $document = $root->ownerDocument;
1441
1442
        $authnStatementEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnStatement');
1443
        $root->appendChild($authnStatementEl);
1444
1445
        $authnStatementEl->setAttribute('AuthnInstant', gmdate('Y-m-d\TH:i:s\Z', $this->authnInstant));
1446
1447
        if ($this->sessionNotOnOrAfter !== null) {
1448
            $authnStatementEl->setAttribute('SessionNotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->sessionNotOnOrAfter));
1449
        }
1450
        if ($this->sessionIndex !== null) {
1451
            $authnStatementEl->setAttribute('SessionIndex', $this->sessionIndex);
1452
        }
1453
1454
        $authnContextEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnContext');
1455
        $authnStatementEl->appendChild($authnContextEl);
1456
1457
        if (!empty($this->authnContextClassRef)) {
1458
            Utils::addString(
1459
                $authnContextEl,
1460
                Constants::NS_SAML,
1461
                'saml:AuthnContextClassRef',
1462
                $this->authnContextClassRef
1463
            );
1464
        }
1465
        if (!empty($this->authnContextDecl)) {
1466
            $this->authnContextDecl->toXML($authnContextEl);
1467
        }
1468
        if (!empty($this->authnContextDeclRef)) {
1469
            Utils::addString(
1470
                $authnContextEl,
1471
                Constants::NS_SAML,
1472
                'saml:AuthnContextDeclRef',
1473
                $this->authnContextDeclRef
0 ignored issues
show
Documentation introduced by
$this->authnContextDeclRef is of type object<SAML2\XML\Chunk>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1474
            );
1475
        }
1476
1477
        Utils::addStrings(
1478
            $authnContextEl,
1479
            Constants::NS_SAML,
1480
            'saml:AuthenticatingAuthority',
1481
            false,
1482
            $this->AuthenticatingAuthority
1483
        );
1484
    }
1485
1486
1487
    /**
1488
     * Add an AttributeStatement-node to the assertion.
1489
     *
1490
     * @param \DOMElement $root The assertion element we should add the subject to.
1491
     */
1492
    private function addAttributeStatement(\DOMElement $root)
1493
    {
1494
        if (empty($this->attributes)) {
1495
            return;
1496
        }
1497
1498
        $document = $root->ownerDocument;
1499
1500
        $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement');
1501
        $root->appendChild($attributeStatement);
1502
1503
        foreach ($this->attributes as $name => $values) {
1504
            $attribute = $document->createElementNS(Constants::NS_SAML, 'saml:Attribute');
1505
            $attributeStatement->appendChild($attribute);
1506
            $attribute->setAttribute('Name', $name);
1507
1508
            if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
1509
                $attribute->setAttribute('NameFormat', $this->nameFormat);
1510
            }
1511
1512
            // make sure eduPersonTargetedID can be handled properly as a NameID
1513
            if ($name === Constants::EPTI_URN_MACE || $name === Constants::EPTI_URN_OID) {
1514
                foreach ($values as $eptiValue) {
1515
                    $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1516
                    $attribute->appendChild($attributeValue);
1517
                    if ($eptiValue instanceof XML\saml\NameID) {
1518
                        $eptiValue->toXML($attributeValue);
1519
                    } elseif ($eptiValue instanceof \DOMNodeList) {
1520
                        $node = $root->ownerDocument->importNode($eptiValue->item(0), true);
1521
                        $attributeValue->appendChild($node);
1522
                    } else {
1523
                        $attributeValue->textContent = $eptiValue;
1524
                    }
1525
                }
1526
1527
                continue;
1528
            }
1529
1530
            foreach ($values as $value) {
1531 View Code Duplication
                if (is_string($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1532
                    $type = 'xs:string';
1533
                } elseif (is_int($value)) {
1534
                    $type = 'xs:integer';
1535
                } else {
1536
                    $type = null;
1537
                }
1538
1539
                $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1540
                $attribute->appendChild($attributeValue);
1541
                if ($type !== null) {
1542
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type);
1543
                }
1544
                if (is_null($value)) {
1545
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:nil', 'true');
1546
                }
1547
1548 View Code Duplication
                if ($value instanceof \DOMNodeList) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1549
                    for ($i = 0; $i < $value->length; $i++) {
1550
                        $node = $document->importNode($value->item($i), true);
1551
                        $attributeValue->appendChild($node);
1552
                    }
1553
                } else {
1554
                    $attributeValue->appendChild($document->createTextNode($value));
1555
                }
1556
            }
1557
        }
1558
    }
1559
1560
1561
    /**
1562
     * Add an EncryptedAttribute Statement-node to the assertion.
1563
     *
1564
     * @param \DOMElement $root The assertion element we should add the Encrypted Attribute Statement to.
1565
     */
1566
    private function addEncryptedAttributeStatement(\DOMElement $root)
1567
    {
1568
        if ($this->requiredEncAttributes == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1569
            return;
1570
        }
1571
1572
        $document = $root->ownerDocument;
1573
1574
        $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement');
1575
        $root->appendChild($attributeStatement);
1576
1577
        foreach ($this->attributes as $name => $values) {
1578
            $document2 = DOMDocumentFactory::create();
1579
            $attribute = $document2->createElementNS(Constants::NS_SAML, 'saml:Attribute');
1580
            $attribute->setAttribute('Name', $name);
1581
            $document2->appendChild($attribute);
1582
1583
            if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
1584
                $attribute->setAttribute('NameFormat', $this->nameFormat);
1585
            }
1586
1587
            foreach ($values as $value) {
1588 View Code Duplication
                if (is_string($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1589
                    $type = 'xs:string';
1590
                } elseif (is_int($value)) {
1591
                    $type = 'xs:integer';
1592
                } else {
1593
                    $type = null;
1594
                }
1595
1596
                $attributeValue = $document2->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1597
                $attribute->appendChild($attributeValue);
1598
                if ($type !== null) {
1599
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type);
1600
                }
1601
1602 View Code Duplication
                if ($value instanceof \DOMNodeList) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1603
                    for ($i = 0; $i < $value->length; $i++) {
1604
                        $node = $document2->importNode($value->item($i), true);
1605
                        $attributeValue->appendChild($node);
1606
                    }
1607
                } else {
1608
                    $attributeValue->appendChild($document2->createTextNode($value));
1609
                }
1610
            }
1611
            /*Once the attribute nodes are built, the are encrypted*/
1612
            $EncAssert = new XMLSecEnc();
1613
            $EncAssert->setNode($document2->documentElement);
1614
            $EncAssert->type = 'http://www.w3.org/2001/04/xmlenc#Element';
1615
            /*
1616
             * Attributes are encrypted with a session key and this one with
1617
             * $EncryptionKey
1618
             */
1619
            $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES256_CBC);
1620
            $symmetricKey->generateSessionKey();
1621
            $EncAssert->encryptKey($this->encryptionKey, $symmetricKey);
0 ignored issues
show
Bug introduced by
It seems like $this->encryptionKey can be null; however, encryptKey() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1622
            $EncrNode = $EncAssert->encryptNode($symmetricKey);
1623
1624
            $EncAttribute = $document->createElementNS(Constants::NS_SAML, 'saml:EncryptedAttribute');
1625
            $attributeStatement->appendChild($EncAttribute);
1626
            $n = $document->importNode($EncrNode, true);
1627
            $EncAttribute->appendChild($n);
1628
        }
1629
    }
1630
}
1631