Completed
Push — master ( 2fa86a...a0de91 )
by Thijs
05:35 queued 03:02
created

Assertion::getAttributesValueTypes()   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
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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 \SAML2\XML\saml\NameID|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 attributes values types as per http://www.w3.org/2001/XMLSchema definitions
178
     * the variable is as an associative array, indexed by attribute name
179
     * 
180
     * when parsing assertion, the variable will be:
181
     * - <attribute name> => array(<Value1's xs type>|null, <xs type Value2>|null, ...)
182
     * array will always have the same size of the array of vaules in $attributes for the same <attribute name>
183
     *
184
     * when generating assertion, the varuable can be:
185
     * - null : backward compatibility
186
     * - <attribute name> => <xs type> : all values for the given attribute will have the same xs type
187
     * - <attribute name> => array(<Value1's xs type>|null, <xs type Value2>|null, ...) : Nth value will have type of the Nth in the array
188
     * 
189
     * @var array multi-dimensional array of array
190
     */
191
    private $attributesValueTypes;
192
193
    /**
194
     * The NameFormat used on all attributes.
195
     *
196
     * If more than one NameFormat is used, this will contain
197
     * the unspecified nameformat.
198
     *
199
     * @var string
200
     */
201
    private $nameFormat;
202
203
    /**
204
     * The private key we should use to sign the assertion.
205
     *
206
     * The private key can be null, in which case the assertion is sent unsigned.
207
     *
208
     * @var XMLSecurityKey|null
209
     */
210
    private $signatureKey;
211
212
    /**
213
     * List of certificates that should be included in the assertion.
214
     *
215
     * @var array
216
     */
217
    private $certificates;
218
219
    /**
220
     * The data needed to verify the signature.
221
     *
222
     * @var array|null
223
     */
224
    private $signatureData;
225
226
    /**
227
     * Boolean that indicates if attributes are encrypted in the
228
     * assertion or not.
229
     *
230
     * @var boolean
231
     */
232
    private $requiredEncAttributes;
233
234
    /**
235
     * The SubjectConfirmation elements of the Subject in the assertion.
236
     *
237
     * @var \SAML2\XML\saml\SubjectConfirmation[].
238
     */
239
    private $SubjectConfirmation;
240
241
    /**
242
     * @var bool
243
     */
244
    protected $wasSignedAtConstruction = false;
245
246
    /**
247
     * @var string|null
248
     */
249
    private $signatureMethod;
250
251
    /**
252
     * Constructor for SAML 2 assertions.
253
     *
254
     * @param \DOMElement|null $xml The input assertion.
255
     * @throws \Exception
256
     */
257
    public function __construct(\DOMElement $xml = null)
258
    {
259
        $this->id = Utils::getContainer()->generateId();
260
        $this->issueInstant = Temporal::getTime();
261
        $this->issuer = '';
262
        $this->authnInstant = Temporal::getTime();
263
        $this->attributes = array();
264
        $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
265
        $this->certificates = array();
266
        $this->AuthenticatingAuthority = array();
267
        $this->SubjectConfirmation = array();
268
269
        if ($xml === null) {
270
            return;
271
        }
272
273
        if (!$xml->hasAttribute('ID')) {
274
            throw new \Exception('Missing ID attribute on SAML assertion.');
275
        }
276
        $this->id = $xml->getAttribute('ID');
277
278
        if ($xml->getAttribute('Version') !== '2.0') {
279
            /* Currently a very strict check. */
280
            throw new \Exception('Unsupported version: ' . $xml->getAttribute('Version'));
281
        }
282
283
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
284
285
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
286
        if (empty($issuer)) {
287
            throw new \Exception('Missing <saml:Issuer> in assertion.');
288
        }
289
        $this->issuer = new XML\saml\Issuer($issuer[0]);
290
        if ($this->issuer->Format === Constants::NAMEID_ENTITY) {
291
            $this->issuer = $this->issuer->value;
292
        }
293
294
        $this->parseSubject($xml);
295
        $this->parseConditions($xml);
296
        $this->parseAuthnStatement($xml);
297
        $this->parseAttributes($xml);
298
        $this->parseEncryptedAttributes($xml);
299
        $this->parseSignature($xml);
300
    }
301
302
    /**
303
     * Parse subject in assertion.
304
     *
305
     * @param \DOMElement $xml The assertion XML element.
306
     * @throws \Exception
307
     */
308
    private function parseSubject(\DOMElement $xml)
309
    {
310
        $subject = Utils::xpQuery($xml, './saml_assertion:Subject');
311
        if (empty($subject)) {
312
            /* No Subject node. */
313
314
            return;
315
        } elseif (count($subject) > 1) {
316
            throw new \Exception('More than one <saml:Subject> in <saml:Assertion>.');
317
        }
318
        $subject = $subject[0];
319
320
        $nameId = Utils::xpQuery(
321
            $subject,
322
            './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData'
323
        );
324
        if (count($nameId) > 1) {
325
            throw new \Exception('More than one <saml:NameID> or <saml:EncryptedID> in <saml:Subject>.');
326
        } elseif (!empty($nameId)) {
327
            $nameId = $nameId[0];
328 View Code Duplication
            if ($nameId->localName === 'EncryptedData') {
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...
329
                /* The NameID element is encrypted. */
330
                $this->encryptedNameId = $nameId;
331
            } else {
332
                $this->nameId = new XML\saml\NameID($nameId);
333
            }
334
        }
335
336
        $subjectConfirmation = Utils::xpQuery($subject, './saml_assertion:SubjectConfirmation');
337
        if (empty($subjectConfirmation) && empty($nameId)) {
338
            throw new \Exception('Missing <saml:SubjectConfirmation> in <saml:Subject>.');
339
        }
340
341
        foreach ($subjectConfirmation as $sc) {
342
            $this->SubjectConfirmation[] = new SubjectConfirmation($sc);
343
        }
344
    }
345
346
    /**
347
     * Parse conditions in assertion.
348
     *
349
     * @param \DOMElement $xml The assertion XML element.
350
     * @throws \Exception
351
     */
352
    private function parseConditions(\DOMElement $xml)
353
    {
354
        $conditions = Utils::xpQuery($xml, './saml_assertion:Conditions');
355
        if (empty($conditions)) {
356
            /* No <saml:Conditions> node. */
357
358
            return;
359
        } elseif (count($conditions) > 1) {
360
            throw new \Exception('More than one <saml:Conditions> in <saml:Assertion>.');
361
        }
362
        $conditions = $conditions[0];
363
364 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...
365
            $notBefore = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotBefore'));
366
            if ($this->notBefore === null || $this->notBefore < $notBefore) {
367
                $this->notBefore = $notBefore;
368
            }
369
        }
370 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...
371
            $notOnOrAfter = Utils::xsDateTimeToTimestamp($conditions->getAttribute('NotOnOrAfter'));
372
            if ($this->notOnOrAfter === null || $this->notOnOrAfter > $notOnOrAfter) {
373
                $this->notOnOrAfter = $notOnOrAfter;
374
            }
375
        }
376
377
        for ($node = $conditions->firstChild; $node !== null; $node = $node->nextSibling) {
378
            if ($node instanceof \DOMText) {
379
                continue;
380
            }
381 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...
382
                throw new \Exception('Unknown namespace of condition: ' . var_export($node->namespaceURI, true));
383
            }
384
            switch ($node->localName) {
385
                case 'AudienceRestriction':
386
                    $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...
387
                    if ($this->validAudiences === null) {
388
                        /* The first (and probably last) AudienceRestriction element. */
389
                        $this->validAudiences = $audiences;
390
                    } else {
391
                        /*
392
                         * The set of AudienceRestriction are ANDed together, so we need
393
                         * the subset that are present in all of them.
394
                         */
395
                        $this->validAudiences = array_intersect($this->validAudiences, $audiences);
396
                    }
397
                    break;
398
                case 'OneTimeUse':
399
                    /* Currently ignored. */
400
                    break;
401
                case 'ProxyRestriction':
402
                    /* Currently ignored. */
403
                    break;
404
                default:
405
                    throw new \Exception('Unknown condition: ' . var_export($node->localName, true));
406
            }
407
        }
408
    }
409
410
    /**
411
     * Parse AuthnStatement in assertion.
412
     *
413
     * @param \DOMElement $xml The assertion XML element.
414
     * @throws \Exception
415
     */
416
    private function parseAuthnStatement(\DOMElement $xml)
417
    {
418
        $authnStatements = Utils::xpQuery($xml, './saml_assertion:AuthnStatement');
419
        if (empty($authnStatements)) {
420
            $this->authnInstant = null;
421
422
            return;
423
        } elseif (count($authnStatements) > 1) {
424
            throw new \Exception('More than one <saml:AuthnStatement> in <saml:Assertion> not supported.');
425
        }
426
        $authnStatement = $authnStatements[0];
427
428
        if (!$authnStatement->hasAttribute('AuthnInstant')) {
429
            throw new \Exception('Missing required AuthnInstant attribute on <saml:AuthnStatement>.');
430
        }
431
        $this->authnInstant = Utils::xsDateTimeToTimestamp($authnStatement->getAttribute('AuthnInstant'));
432
433
        if ($authnStatement->hasAttribute('SessionNotOnOrAfter')) {
434
            $this->sessionNotOnOrAfter = Utils::xsDateTimeToTimestamp($authnStatement->getAttribute('SessionNotOnOrAfter'));
435
        }
436
437
        if ($authnStatement->hasAttribute('SessionIndex')) {
438
            $this->sessionIndex = $authnStatement->getAttribute('SessionIndex');
439
        }
440
441
        $this->parseAuthnContext($authnStatement);
442
    }
443
444
    /**
445
     * Parse AuthnContext in AuthnStatement.
446
     *
447
     * @param \DOMElement $authnStatementEl
448
     * @throws \Exception
449
     */
450
    private function parseAuthnContext(\DOMElement $authnStatementEl)
451
    {
452
        // Get the AuthnContext element
453
        $authnContexts = Utils::xpQuery($authnStatementEl, './saml_assertion:AuthnContext');
454
        if (count($authnContexts) > 1) {
455
            throw new \Exception('More than one <saml:AuthnContext> in <saml:AuthnStatement>.');
456
        } elseif (empty($authnContexts)) {
457
            throw new \Exception('Missing required <saml:AuthnContext> in <saml:AuthnStatement>.');
458
        }
459
        $authnContextEl = $authnContexts[0];
460
461
        // Get the AuthnContextDeclRef (if available)
462
        $authnContextDeclRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDeclRef');
463
        if (count($authnContextDeclRefs) > 1) {
464
            throw new \Exception(
465
                'More than one <saml:AuthnContextDeclRef> found?'
466
            );
467
        } elseif (count($authnContextDeclRefs) === 1) {
468
            $this->setAuthnContextDeclRef(trim($authnContextDeclRefs[0]->textContent));
469
        }
470
471
        // Get the AuthnContextDecl (if available)
472
        $authnContextDecls = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextDecl');
473
        if (count($authnContextDecls) > 1) {
474
            throw new \Exception(
475
                'More than one <saml:AuthnContextDecl> found?'
476
            );
477
        } elseif (count($authnContextDecls) === 1) {
478
            $this->setAuthnContextDecl(new Chunk($authnContextDecls[0]));
479
        }
480
481
        // Get the AuthnContextClassRef (if available)
482
        $authnContextClassRefs = Utils::xpQuery($authnContextEl, './saml_assertion:AuthnContextClassRef');
483
        if (count($authnContextClassRefs) > 1) {
484
            throw new \Exception('More than one <saml:AuthnContextClassRef> in <saml:AuthnContext>.');
485
        } elseif (count($authnContextClassRefs) === 1) {
486
            $this->setAuthnContextClassRef(trim($authnContextClassRefs[0]->textContent));
487
        }
488
489
        // Constraint from XSD: MUST have one of the three
490
        if (empty($this->authnContextClassRef) && empty($this->authnContextDecl) && empty($this->authnContextDeclRef)) {
491
            throw new \Exception(
492
                'Missing either <saml:AuthnContextClassRef> or <saml:AuthnContextDeclRef> or <saml:AuthnContextDecl>'
493
            );
494
        }
495
496
        $this->AuthenticatingAuthority = Utils::extractStrings(
497
            $authnContextEl,
498
            Constants::NS_SAML,
499
            'AuthenticatingAuthority'
500
        );
501
    }
502
503
    /**
504
     * Parse attribute statements in assertion.
505
     *
506
     * @param \DOMElement $xml The XML element with the assertion.
507
     * @throws \Exception
508
     */
509
    private function parseAttributes(\DOMElement $xml)
510
    {
511
        $firstAttribute = true;
512
        $attributes = Utils::xpQuery($xml, './saml_assertion:AttributeStatement/saml_assertion:Attribute');
513
        foreach ($attributes as $attribute) {
514
            if (!$attribute->hasAttribute('Name')) {
515
                throw new \Exception('Missing name on <saml:Attribute> element.');
516
            }
517
            $name = $attribute->getAttribute('Name');
518
519
            if ($attribute->hasAttribute('NameFormat')) {
520
                $nameFormat = $attribute->getAttribute('NameFormat');
521
            } else {
522
                $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
523
            }
524
525
            if ($firstAttribute) {
526
                $this->nameFormat = $nameFormat;
527
                $firstAttribute = false;
528
            } else {
529
                if ($this->nameFormat !== $nameFormat) {
530
                    $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
531
                }
532
            }
533
534
            if (!array_key_exists($name, $this->attributes)) {
535
                $this->attributes[$name] = array();
536
                $this->attributesValueTypes[$name] = array();
537
            }
538
539
            $this->parseAttributeValue($attribute, $name);
540
        }
541
    }
542
543
    /**
544
     * @param \DOMNode $attribute
545
     * @param string   $attributeName
546
     */
547
    private function parseAttributeValue($attribute, $attributeName)
548
    {
549
        /** @var \DOMElement[] $values */
550
        $values = Utils::xpQuery($attribute, './saml_assertion:AttributeValue');
551
552
        if ($attributeName === Constants::EPTI_URN_MACE || $attributeName === Constants::EPTI_URN_OID) {
553
            foreach ($values as $index => $eptiAttributeValue) {
554
                $eptiNameId = Utils::xpQuery($eptiAttributeValue, './saml_assertion:NameID');
555
556
                if (count($eptiNameId) !== 1) {
557
                    throw new RuntimeException(sprintf(
558
                        'A "%s" (EPTI) attribute value must be a NameID, none found for value no. "%d"',
559
                        $attributeName,
560
                        $index
561
                    ));
562
                }
563
564
                $this->attributes[$attributeName][] = new XML\saml\NameID($eptiNameId[0]);
565
            }
566
567
            return;
568
        }
569
570
        foreach ($values as $value) {
571
            $hasNonTextChildElements = false;
572
            foreach ($value->childNodes as $childNode) {
573
                /** @var \DOMNode $childNode */
574
                if ($childNode->nodeType !== XML_TEXT_NODE) {
575
                    $hasNonTextChildElements = true;
576
                    break;
577
                }
578
            }
579
580
            $type = $value->getAttribute('xsi:type');
581
            if ($type === '') {
582
                $type = null;
583
            }
584
            $this->attributesValueTypes[$attributeName][] = $type;
585
586
            if ($hasNonTextChildElements) {
587
                $this->attributes[$attributeName][] = $value->childNodes;
588
                continue;
589
            }
590
            
591
            if ($type === 'xs:integer') {
592
                $this->attributes[$attributeName][] = (int)$value->textContent;
593
            } else {
594
                $this->attributes[$attributeName][] = trim($value->textContent);
595
            }
596
        }
597
    }
598
599
    /**
600
     * Parse encrypted attribute statements in assertion.
601
     *
602
     * @param \DOMElement $xml The XML element with the assertion.
603
     */
604
    private function parseEncryptedAttributes(\DOMElement $xml)
605
    {
606
        $this->encryptedAttributes = Utils::xpQuery(
607
            $xml,
608
            './saml_assertion:AttributeStatement/saml_assertion:EncryptedAttribute'
609
        );
610
    }
611
612
    /**
613
     * Parse signature on assertion.
614
     *
615
     * @param \DOMElement $xml The assertion XML element.
616
     */
617
    private function parseSignature(\DOMElement $xml)
618
    {
619
        /** @var null|\DOMAttr $signatureMethod */
620
        $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
621
622
        /* Validate the signature element of the message. */
623
        $sig = Utils::validateElement($xml);
624
        if ($sig !== false) {
625
            $this->wasSignedAtConstruction = true;
626
            $this->certificates = $sig['Certificates'];
627
            $this->signatureData = $sig;
628
            $this->signatureMethod = $signatureMethod[0]->value;
629
        }
630
    }
631
632
    /**
633
     * Validate this assertion against a public key.
634
     *
635
     * If no signature was present on the assertion, we will return false.
636
     * Otherwise, true will be returned. An exception is thrown if the
637
     * signature validation fails.
638
     *
639
     * @param  XMLSecurityKey $key The key we should check against.
640
     * @return boolean        true if successful, false if it is unsigned.
641
     */
642
    public function validate(XMLSecurityKey $key)
643
    {
644
        assert($key->type === \RobRichards\XMLSecLibs\XMLSecurityKey::RSA_SHA1);
645
646
        if ($this->signatureData === null) {
647
            return false;
648
        }
649
650
        Utils::validateSignature($this->signatureData, $key);
651
652
        return true;
653
    }
654
655
    /**
656
     * Retrieve the identifier of this assertion.
657
     *
658
     * @return string The identifier of this assertion.
659
     */
660
    public function getId()
661
    {
662
        return $this->id;
663
    }
664
665
    /**
666
     * Set the identifier of this assertion.
667
     *
668
     * @param string $id The new identifier of this assertion.
669
     */
670
    public function setId($id)
671
    {
672
        assert(is_string($id));
673
674
        $this->id = $id;
675
    }
676
677
    /**
678
     * Retrieve the issue timestamp of this assertion.
679
     *
680
     * @return int The issue timestamp of this assertion, as an UNIX timestamp.
681
     */
682
    public function getIssueInstant()
683
    {
684
        return $this->issueInstant;
685
    }
686
687
    /**
688
     * Set the issue timestamp of this assertion.
689
     *
690
     * @param int $issueInstant The new issue timestamp of this assertion, as an UNIX timestamp.
691
     */
692
    public function setIssueInstant($issueInstant)
693
    {
694
        assert(is_int($issueInstant));
695
696
        $this->issueInstant = $issueInstant;
697
    }
698
699
    /**
700
     * Retrieve the issuer if this assertion.
701
     *
702
     * @return string|\SAML2\XML\saml\Issuer The issuer of this assertion.
703
     */
704
    public function getIssuer()
705
    {
706
        return $this->issuer;
707
    }
708
709
    /**
710
     * Set the issuer of this message.
711
     *
712
     * @param string|\SAML2\XML\saml\Issuer $issuer The new issuer of this assertion.
713
     */
714
    public function setIssuer($issuer)
715
    {
716
        assert(is_string($issuer) || $issuer instanceof \SAML2\XML\saml\Issuer);
717
718
        $this->issuer = $issuer;
719
    }
720
721
    /**
722
     * Retrieve the NameId of the subject in the assertion.
723
     *
724
     * @return \SAML2\XML\saml\NameID|null The name identifier of the assertion.
725
     * @throws \Exception
726
     */
727
    public function getNameId()
728
    {
729
        if ($this->encryptedNameId !== null) {
730
            throw new \Exception('Attempted to retrieve encrypted NameID without decrypting it first.');
731
        }
732
733
        return $this->nameId;
734
    }
735
736
    /**
737
     * Set the NameId of the subject in the assertion.
738
     *
739
     * The NameId must be a \SAML2\XML\saml\NameID object or an array in the format accepted by
740
     * \SAML2\Utils::addNameId() (an array, deprecated).
741
     *
742
     * @see \SAML2\Utils::addNameId()
743
     * @param \SAML2\XML\saml\NameID|array|null $nameId The name identifier of the assertion.
744
     */
745 View Code Duplication
    public function setNameId($nameId)
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...
746
    {
747
        assert(is_array($nameId) || is_null($nameId) || is_a($nameId, "\SAML2\XML\saml\NameID"));
748
749
        if (is_array($nameId)) {
750
            $nameId = XML\saml\NameID::fromArray($nameId);
0 ignored issues
show
Deprecated Code introduced by
The method SAML2\XML\saml\NameIDType::fromArray() has been deprecated.

This method has been deprecated.

Loading history...
751
        }
752
        $this->nameId = $nameId;
753
    }
754
755
    /**
756
     * Check whether the NameId is encrypted.
757
     *
758
     * @return true if the NameId is encrypted, false if not.
759
     */
760
    public function isNameIdEncrypted()
761
    {
762
        return $this->encryptedNameId !== null;
763
    }
764
765
    /**
766
     * Encrypt the NameID in the Assertion.
767
     *
768
     * @param XMLSecurityKey $key The encryption key.
769
     */
770 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...
771
    {
772
        /* First create a XML representation of the NameID. */
773
        $doc = DOMDocumentFactory::create();
774
        $root = $doc->createElement('root');
775
        $doc->appendChild($root);
776
        $this->nameId->toXML($root);
777
        $nameId = $root->firstChild;
778
779
        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...
780
781
        /* Encrypt the NameID. */
782
        $enc = new XMLSecEnc();
783
        $enc->setNode($nameId);
784
        // @codingStandardsIgnoreStart
785
        $enc->type = XMLSecEnc::Element;
786
        // @codingStandardsIgnoreEnd
787
788
        $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
789
        $symmetricKey->generateSessionKey();
790
        $enc->encryptKey($key, $symmetricKey);
791
792
        $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...
793
        $this->nameId = null;
794
    }
795
796
    /**
797
     * Decrypt the NameId of the subject in the assertion.
798
     *
799
     * @param XMLSecurityKey $key       The decryption key.
800
     * @param array          $blacklist Blacklisted decryption algorithms.
801
     */
802 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...
803
    {
804
        if ($this->encryptedNameId === null) {
805
            /* No NameID to decrypt. */
806
807
            return;
808
        }
809
810
        $nameId = Utils::decryptElement($this->encryptedNameId, $key, $blacklist);
811
        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...
812
        $this->nameId = new XML\saml\NameID($nameId);
813
814
        $this->encryptedNameId = null;
815
    }
816
817
    /**
818
     * Did this Assertion contain encrypted Attributes?
819
     *
820
     * @return bool
821
     */
822
    public function hasEncryptedAttributes()
823
    {
824
        return $this->encryptedAttributes !== null;
825
    }
826
827
    /**
828
     * Decrypt the assertion attributes.
829
     *
830
     * @param XMLSecurityKey $key
831
     * @param array $blacklist
832
     * @throws \Exception
833
     */
834
    public function decryptAttributes(XMLSecurityKey $key, array $blacklist = array())
835
    {
836
        if ($this->encryptedAttributes === null) {
837
            return;
838
        }
839
        $firstAttribute = true;
840
        $attributes = $this->encryptedAttributes;
841
        foreach ($attributes as $attributeEnc) {
842
            /*Decrypt node <EncryptedAttribute>*/
843
            $attribute = Utils::decryptElement(
844
                $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...
845
                $key,
846
                $blacklist
847
            );
848
849
            if (!$attribute->hasAttribute('Name')) {
850
                throw new \Exception('Missing name on <saml:Attribute> element.');
851
            }
852
            $name = $attribute->getAttribute('Name');
853
854
            if ($attribute->hasAttribute('NameFormat')) {
855
                $nameFormat = $attribute->getAttribute('NameFormat');
856
            } else {
857
                $nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
858
            }
859
860
            if ($firstAttribute) {
861
                $this->nameFormat = $nameFormat;
862
                $firstAttribute = false;
863
            } else {
864
                if ($this->nameFormat !== $nameFormat) {
865
                    $this->nameFormat = Constants::NAMEFORMAT_UNSPECIFIED;
866
                }
867
            }
868
869
            if (!array_key_exists($name, $this->attributes)) {
870
                $this->attributes[$name] = array();
871
            }
872
873
            $this->parseAttributeValue($attribute, $name);
874
        }
875
    }
876
877
    /**
878
     * Retrieve the earliest timestamp this assertion is valid.
879
     *
880
     * This function returns null if there are no restrictions on how early the
881
     * assertion can be used.
882
     *
883
     * @return int|null The earliest timestamp this assertion is valid.
884
     */
885
    public function getNotBefore()
886
    {
887
        return $this->notBefore;
888
    }
889
890
    /**
891
     * Set the earliest timestamp this assertion can be used.
892
     *
893
     * Set this to null if no limit is required.
894
     *
895
     * @param int|null $notBefore The earliest timestamp this assertion is valid.
896
     */
897
    public function setNotBefore($notBefore)
898
    {
899
        assert(is_int($notBefore) || is_null($notBefore));
900
901
        $this->notBefore = $notBefore;
902
    }
903
904
    /**
905
     * Retrieve the expiration timestamp of this assertion.
906
     *
907
     * This function returns null if there are no restrictions on how
908
     * late the assertion can be used.
909
     *
910
     * @return int|null The latest timestamp this assertion is valid.
911
     */
912
    public function getNotOnOrAfter()
913
    {
914
        return $this->notOnOrAfter;
915
    }
916
917
    /**
918
     * Set the expiration timestamp of this assertion.
919
     *
920
     * Set this to null if no limit is required.
921
     *
922
     * @param int|null $notOnOrAfter The latest timestamp this assertion is valid.
923
     */
924
    public function setNotOnOrAfter($notOnOrAfter)
925
    {
926
        assert(is_int($notOnOrAfter) || is_null($notOnOrAfter));
927
928
        $this->notOnOrAfter = $notOnOrAfter;
929
    }
930
931
    /**
932
     * Set $EncryptedAttributes if attributes will send encrypted
933
     *
934
     * @param boolean $ea true to encrypt attributes in the assertion.
935
     */
936
    public function setEncryptedAttributes($ea)
937
    {
938
        $this->requiredEncAttributes = $ea;
939
    }
940
941
    /**
942
     * Retrieve the audiences that are allowed to receive this assertion.
943
     *
944
     * This may be null, in which case all audiences are allowed.
945
     *
946
     * @return array|null The allowed audiences.
947
     */
948
    public function getValidAudiences()
949
    {
950
        return $this->validAudiences;
951
    }
952
953
    /**
954
     * Set the audiences that are allowed to receive this assertion.
955
     *
956
     * This may be null, in which case all audiences are allowed.
957
     *
958
     * @param array|null $validAudiences The allowed audiences.
959
     */
960
    public function setValidAudiences(array $validAudiences = null)
961
    {
962
        $this->validAudiences = $validAudiences;
963
    }
964
965
    /**
966
     * Retrieve the AuthnInstant of the assertion.
967
     *
968
     * @return int|null The timestamp the user was authenticated, or NULL if the user isn't authenticated.
969
     */
970
    public function getAuthnInstant()
971
    {
972
        return $this->authnInstant;
973
    }
974
975
976
    /**
977
     * Set the AuthnInstant of the assertion.
978
     *
979
     * @param int|null $authnInstant Timestamp the user was authenticated, or NULL if we don't want an AuthnStatement.
980
     */
981
    public function setAuthnInstant($authnInstant)
982
    {
983
        assert(is_int($authnInstant) || is_null($authnInstant));
984
985
        $this->authnInstant = $authnInstant;
986
    }
987
988
    /**
989
     * Retrieve the session expiration timestamp.
990
     *
991
     * This function returns null if there are no restrictions on the
992
     * session lifetime.
993
     *
994
     * @return int|null The latest timestamp this session is valid.
995
     */
996
    public function getSessionNotOnOrAfter()
997
    {
998
        return $this->sessionNotOnOrAfter;
999
    }
1000
1001
    /**
1002
     * Set the session expiration timestamp.
1003
     *
1004
     * Set this to null if no limit is required.
1005
     *
1006
     * @param int|null $sessionNotOnOrAfter The latest timestamp this session is valid.
1007
     */
1008
    public function setSessionNotOnOrAfter($sessionNotOnOrAfter)
1009
    {
1010
        assert(is_int($sessionNotOnOrAfter) || is_null($sessionNotOnOrAfter));
1011
1012
        $this->sessionNotOnOrAfter = $sessionNotOnOrAfter;
1013
    }
1014
1015
    /**
1016
     * Retrieve the session index of the user at the IdP.
1017
     *
1018
     * @return string|null The session index of the user at the IdP.
1019
     */
1020
    public function getSessionIndex()
1021
    {
1022
        return $this->sessionIndex;
1023
    }
1024
1025
    /**
1026
     * Set the session index of the user at the IdP.
1027
     *
1028
     * Note that the authentication context must be set before the
1029
     * session index can be inluded in the assertion.
1030
     *
1031
     * @param string|null $sessionIndex The session index of the user at the IdP.
1032
     */
1033
    public function setSessionIndex($sessionIndex)
1034
    {
1035
        assert(is_string($sessionIndex) || is_null($sessionIndex));
1036
1037
        $this->sessionIndex = $sessionIndex;
1038
    }
1039
1040
    /**
1041
     * Retrieve the authentication method used to authenticate the user.
1042
     *
1043
     * This will return null if no authentication statement was
1044
     * included in the assertion.
1045
     *
1046
     * Note that this returns either the AuthnContextClassRef or the AuthnConextDeclRef, whose definition overlaps
1047
     * but is slightly different (consult the specification for more information).
1048
     * This was done to work around an old bug of Shibboleth ( https://bugs.internet2.edu/jira/browse/SIDP-187 ).
1049
     * Should no longer be required, please use either getAuthnConextClassRef or getAuthnContextDeclRef.
1050
     *
1051
     * @deprecated use getAuthnContextClassRef
1052
     * @return string|null The authentication method.
1053
     */
1054
    public function getAuthnContext()
1055
    {
1056
        if (!empty($this->authnContextClassRef)) {
1057
            return $this->authnContextClassRef;
1058
        }
1059
        if (!empty($this->authnContextDeclRef)) {
1060
            return $this->authnContextDeclRef;
1061
        }
1062
        return null;
1063
    }
1064
1065
    /**
1066
     * Set the authentication method used to authenticate the user.
1067
     *
1068
     * If this is set to null, no authentication statement will be
1069
     * included in the assertion. The default is null.
1070
     *
1071
     * @deprecated use setAuthnContextClassRef
1072
     * @param string|null $authnContext The authentication method.
1073
     */
1074
    public function setAuthnContext($authnContext)
1075
    {
1076
        $this->setAuthnContextClassRef($authnContext);
1077
    }
1078
1079
    /**
1080
     * Retrieve the authentication method used to authenticate the user.
1081
     *
1082
     * This will return null if no authentication statement was
1083
     * included in the assertion.
1084
     *
1085
     * @return string|null The authentication method.
1086
     */
1087
    public function getAuthnContextClassRef()
1088
    {
1089
        return $this->authnContextClassRef;
1090
    }
1091
1092
    /**
1093
     * Set the authentication method used to authenticate the user.
1094
     *
1095
     * If this is set to null, no authentication statement will be
1096
     * included in the assertion. The default is null.
1097
     *
1098
     * @param string|null $authnContextClassRef The authentication method.
1099
     */
1100
    public function setAuthnContextClassRef($authnContextClassRef)
1101
    {
1102
        assert(is_string($authnContextClassRef) || is_null($authnContextClassRef));
1103
1104
        $this->authnContextClassRef = $authnContextClassRef;
1105
    }
1106
1107
    /**
1108
     * Set the authentication context declaration.
1109
     *
1110
     * @param \SAML2\XML\Chunk $authnContextDecl
1111
     * @throws \Exception
1112
     */
1113
    public function setAuthnContextDecl(Chunk $authnContextDecl)
1114
    {
1115
        if (!empty($this->authnContextDeclRef)) {
1116
            throw new \Exception(
1117
                'AuthnContextDeclRef is already registered! May only have either a Decl or a DeclRef, not both!'
1118
            );
1119
        }
1120
1121
        $this->authnContextDecl = $authnContextDecl;
1122
    }
1123
1124
    /**
1125
     * Get the authentication context declaration.
1126
     *
1127
     * See:
1128
     * @url http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
1129
     *
1130
     * @return \SAML2\XML\Chunk|null
1131
     */
1132
    public function getAuthnContextDecl()
1133
    {
1134
        return $this->authnContextDecl;
1135
    }
1136
1137
    /**
1138
     * Set the authentication context declaration reference.
1139
     *
1140
     * @param string $authnContextDeclRef
1141
     * @throws \Exception
1142
     */
1143
    public function setAuthnContextDeclRef($authnContextDeclRef)
1144
    {
1145
        if (!empty($this->authnContextDecl)) {
1146
            throw new \Exception(
1147
                'AuthnContextDecl is already registered! May only have either a Decl or a DeclRef, not both!'
1148
            );
1149
        }
1150
1151
        $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...
1152
    }
1153
1154
    /**
1155
     * Get the authentication context declaration reference.
1156
     * URI reference that identifies an authentication context declaration.
1157
     *
1158
     * The URI reference MAY directly resolve into an XML document containing the referenced declaration.
1159
     *
1160
     * @return string
1161
     */
1162
    public function getAuthnContextDeclRef()
1163
    {
1164
        return $this->authnContextDeclRef;
1165
    }
1166
1167
    /**
1168
     * Retrieve the AuthenticatingAuthority.
1169
     *
1170
     *
1171
     * @return array
1172
     */
1173
    public function getAuthenticatingAuthority()
1174
    {
1175
        return $this->AuthenticatingAuthority;
1176
    }
1177
1178
    /**
1179
     * Set the AuthenticatingAuthority
1180
     *
1181
     *
1182
     * @param array.
1183
     */
1184
    public function setAuthenticatingAuthority($authenticatingAuthority)
1185
    {
1186
        $this->AuthenticatingAuthority = $authenticatingAuthority;
1187
    }
1188
1189
    /**
1190
     * Retrieve all attributes.
1191
     *
1192
     * @return array All attributes, as an associative array.
1193
     */
1194
    public function getAttributes()
1195
    {
1196
        return $this->attributes;
1197
    }
1198
1199
    /**
1200
     * Replace all attributes.
1201
     *
1202
     * @param array $attributes All new attributes, as an associative array.
1203
     */
1204
    public function setAttributes(array $attributes)
1205
    {
1206
        $this->attributes = $attributes;
1207
    }
1208
1209
    /**
1210
     * Retrieve all attributes value types.
1211
     *
1212
     * @return array All attributes value types, as an associative array.
1213
     */
1214
    public function getAttributesValueTypes()
1215
    {
1216
        return $this->attributesValueTypes;
1217
    }
1218
1219
    /**
1220
     * Replace all attributes value types..
1221
     *
1222
     * @param array $attributes All new attribute value types, as an associative array.
0 ignored issues
show
Documentation introduced by
There is no parameter named $attributes. Did you maybe mean $attributesValueTypes?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1223
     */
1224
    public function setAttributesValueTypes(array $attributesValueTypes)
1225
    {
1226
        $this->attributesValueTypes = $attributesValueTypes;
1227
    }
1228
1229
    /**
1230
     * Retrieve the NameFormat used on all attributes.
1231
     *
1232
     * If more than one NameFormat is used in the received attributes, this
1233
     * returns the unspecified NameFormat.
1234
     *
1235
     * @return string The NameFormat used on all attributes.
1236
     */
1237
    public function getAttributeNameFormat()
1238
    {
1239
        return $this->nameFormat;
1240
    }
1241
1242
    /**
1243
     * Set the NameFormat used on all attributes.
1244
     *
1245
     * @param string $nameFormat The NameFormat used on all attributes.
1246
     */
1247
    public function setAttributeNameFormat($nameFormat)
1248
    {
1249
        assert(is_string($nameFormat));
1250
1251
        $this->nameFormat = $nameFormat;
1252
    }
1253
1254
    /**
1255
     * Retrieve the SubjectConfirmation elements we have in our Subject element.
1256
     *
1257
     * @return array Array of \SAML2\XML\saml\SubjectConfirmation elements.
1258
     */
1259
    public function getSubjectConfirmation()
1260
    {
1261
        return $this->SubjectConfirmation;
1262
    }
1263
1264
    /**
1265
     * Set the SubjectConfirmation elements that should be included in the assertion.
1266
     *
1267
     * @param array $SubjectConfirmation Array of \SAML2\XML\saml\SubjectConfirmation elements.
1268
     */
1269
    public function setSubjectConfirmation(array $SubjectConfirmation)
1270
    {
1271
        $this->SubjectConfirmation = $SubjectConfirmation;
1272
    }
1273
1274
    /**
1275
     * Retrieve the private key we should use to sign the assertion.
1276
     *
1277
     * @return XMLSecurityKey|null The key, or NULL if no key is specified.
1278
     */
1279
    public function getSignatureKey()
1280
    {
1281
        return $this->signatureKey;
1282
    }
1283
1284
    /**
1285
     * Set the private key we should use to sign the assertion.
1286
     *
1287
     * If the key is null, the assertion will be sent unsigned.
1288
     *
1289
     * @param XMLSecurityKey|null $signatureKey
1290
     */
1291
    public function setSignatureKey(XMLSecurityKey $signatureKey = null)
1292
    {
1293
        $this->signatureKey = $signatureKey;
1294
    }
1295
1296
    /**
1297
     * Return the key we should use to encrypt the assertion.
1298
     *
1299
     * @return XMLSecurityKey|null The key, or NULL if no key is specified..
1300
     *
1301
     */
1302
    public function getEncryptionKey()
1303
    {
1304
        return $this->encryptionKey;
1305
    }
1306
1307
    /**
1308
     * Set the private key we should use to encrypt the attributes.
1309
     *
1310
     * @param XMLSecurityKey|null $Key
1311
     */
1312
    public function setEncryptionKey(XMLSecurityKey $Key = null)
1313
    {
1314
        $this->encryptionKey = $Key;
1315
    }
1316
1317
    /**
1318
     * Set the certificates that should be included in the assertion.
1319
     *
1320
     * The certificates should be strings with the PEM encoded data.
1321
     *
1322
     * @param array $certificates An array of certificates.
1323
     */
1324
    public function setCertificates(array $certificates)
1325
    {
1326
        $this->certificates = $certificates;
1327
    }
1328
1329
    /**
1330
     * Retrieve the certificates that are included in the assertion.
1331
     *
1332
     * @return array An array of certificates.
1333
     */
1334
    public function getCertificates()
1335
    {
1336
        return $this->certificates;
1337
    }
1338
1339
    /**
1340
     * @return bool
1341
     */
1342
    public function getWasSignedAtConstruction()
1343
    {
1344
        return $this->wasSignedAtConstruction;
1345
    }
1346
1347
    /**
1348
     * @return null|string
1349
     */
1350
    public function getSignatureMethod()
1351
    {
1352
        return $this->signatureMethod;
1353
    }
1354
1355
    /**
1356
     * Convert this assertion to an XML element.
1357
     *
1358
     * @param  \DOMNode|null $parentElement The DOM node the assertion should be created in.
1359
     * @return \DOMElement   This assertion.
1360
     */
1361
    public function toXML(\DOMNode $parentElement = null)
1362
    {
1363 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...
1364
            $document = DOMDocumentFactory::create();
1365
            $parentElement = $document;
1366
        } else {
1367
            $document = $parentElement->ownerDocument;
1368
        }
1369
1370
        $root = $document->createElementNS(Constants::NS_SAML, 'saml:' . 'Assertion');
1371
        $parentElement->appendChild($root);
1372
1373
        /* Ugly hack to add another namespace declaration to the root element. */
1374
        $root->setAttributeNS(Constants::NS_SAMLP, 'samlp:tmp', 'tmp');
1375
        $root->removeAttributeNS(Constants::NS_SAMLP, 'tmp');
1376
        $root->setAttributeNS(Constants::NS_XSI, 'xsi:tmp', 'tmp');
1377
        $root->removeAttributeNS(Constants::NS_XSI, 'tmp');
1378
        $root->setAttributeNS(Constants::NS_XS, 'xs:tmp', 'tmp');
1379
        $root->removeAttributeNS(Constants::NS_XS, 'tmp');
1380
1381
        $root->setAttribute('ID', $this->id);
1382
        $root->setAttribute('Version', '2.0');
1383
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
1384
1385
        if (is_string($this->issuer)) {
1386
            $issuer = Utils::addString($root, Constants::NS_SAML, 'saml:Issuer', $this->issuer);
1387
        } elseif ($this->issuer instanceof \SAML2\XML\saml\Issuer) {
1388
            $issuer = $this->issuer->toXML($root);
1389
        }
1390
1391
        $this->addSubject($root);
1392
        $this->addConditions($root);
1393
        $this->addAuthnStatement($root);
1394
        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...
1395
            $this->addAttributeStatement($root);
1396
        } else {
1397
            $this->addEncryptedAttributeStatement($root);
1398
        }
1399
1400
        if ($this->signatureKey !== null) {
1401
            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...
1402
        }
1403
1404
        return $root;
1405
    }
1406
1407
    /**
1408
     * Add a Subject-node to the assertion.
1409
     *
1410
     * @param \DOMElement $root The assertion element we should add the subject to.
1411
     */
1412 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...
1413
    {
1414
        if ($this->nameId === null && $this->encryptedNameId === null) {
1415
            /* We don't have anything to create a Subject node for. */
1416
1417
            return;
1418
        }
1419
1420
        $subject = $root->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:Subject');
1421
        $root->appendChild($subject);
1422
1423
        if ($this->encryptedNameId === null) {
1424
            $this->nameId->toXML($subject);
1425
        } else {
1426
            $eid = $subject->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:' . 'EncryptedID');
1427
            $subject->appendChild($eid);
1428
            $eid->appendChild($subject->ownerDocument->importNode($this->encryptedNameId, true));
1429
        }
1430
1431
        foreach ($this->SubjectConfirmation as $sc) {
1432
            $sc->toXML($subject);
1433
        }
1434
    }
1435
1436
1437
    /**
1438
     * Add a Conditions-node to the assertion.
1439
     *
1440
     * @param \DOMElement $root The assertion element we should add the conditions to.
1441
     */
1442
    private function addConditions(\DOMElement $root)
1443
    {
1444
        $document = $root->ownerDocument;
1445
1446
        $conditions = $document->createElementNS(Constants::NS_SAML, 'saml:Conditions');
1447
        $root->appendChild($conditions);
1448
1449
        if ($this->notBefore !== null) {
1450
            $conditions->setAttribute('NotBefore', gmdate('Y-m-d\TH:i:s\Z', $this->notBefore));
1451
        }
1452
        if ($this->notOnOrAfter !== null) {
1453
            $conditions->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter));
1454
        }
1455
1456
        if ($this->validAudiences !== null) {
1457
            $ar = $document->createElementNS(Constants::NS_SAML, 'saml:AudienceRestriction');
1458
            $conditions->appendChild($ar);
1459
1460
            Utils::addStrings($ar, Constants::NS_SAML, 'saml:Audience', false, $this->validAudiences);
1461
        }
1462
    }
1463
1464
1465
    /**
1466
     * Add a AuthnStatement-node to the assertion.
1467
     *
1468
     * @param \DOMElement $root The assertion element we should add the authentication statement to.
1469
     */
1470
    private function addAuthnStatement(\DOMElement $root)
1471
    {
1472
        if ($this->authnInstant === null ||
1473
            (
1474
                $this->authnContextClassRef === null &&
1475
                $this->authnContextDecl === null &&
1476
                $this->authnContextDeclRef === null
1477
            )
1478
        ) {
1479
            /* No authentication context or AuthnInstant => no authentication statement. */
1480
1481
            return;
1482
        }
1483
1484
        $document = $root->ownerDocument;
1485
1486
        $authnStatementEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnStatement');
1487
        $root->appendChild($authnStatementEl);
1488
1489
        $authnStatementEl->setAttribute('AuthnInstant', gmdate('Y-m-d\TH:i:s\Z', $this->authnInstant));
1490
1491
        if ($this->sessionNotOnOrAfter !== null) {
1492
            $authnStatementEl->setAttribute('SessionNotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->sessionNotOnOrAfter));
1493
        }
1494
        if ($this->sessionIndex !== null) {
1495
            $authnStatementEl->setAttribute('SessionIndex', $this->sessionIndex);
1496
        }
1497
1498
        $authnContextEl = $document->createElementNS(Constants::NS_SAML, 'saml:AuthnContext');
1499
        $authnStatementEl->appendChild($authnContextEl);
1500
1501
        if (!empty($this->authnContextClassRef)) {
1502
            Utils::addString(
1503
                $authnContextEl,
1504
                Constants::NS_SAML,
1505
                'saml:AuthnContextClassRef',
1506
                $this->authnContextClassRef
1507
            );
1508
        }
1509
        if (!empty($this->authnContextDecl)) {
1510
            $this->authnContextDecl->toXML($authnContextEl);
1511
        }
1512
        if (!empty($this->authnContextDeclRef)) {
1513
            Utils::addString(
1514
                $authnContextEl,
1515
                Constants::NS_SAML,
1516
                'saml:AuthnContextDeclRef',
1517
                $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...
1518
            );
1519
        }
1520
1521
        Utils::addStrings(
1522
            $authnContextEl,
1523
            Constants::NS_SAML,
1524
            'saml:AuthenticatingAuthority',
1525
            false,
1526
            $this->AuthenticatingAuthority
1527
        );
1528
    }
1529
1530
1531
    /**
1532
     * Add an AttributeStatement-node to the assertion.
1533
     *
1534
     * @param \DOMElement $root The assertion element we should add the subject to.
1535
     */
1536
    private function addAttributeStatement(\DOMElement $root)
1537
    {
1538
        if (empty($this->attributes)) {
1539
            return;
1540
        }
1541
1542
        $document = $root->ownerDocument;
1543
1544
        $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement');
1545
        $root->appendChild($attributeStatement);
1546
1547
        foreach ($this->attributes as $name => $values) {
1548
            $attribute = $document->createElementNS(Constants::NS_SAML, 'saml:Attribute');
1549
            $attributeStatement->appendChild($attribute);
1550
            $attribute->setAttribute('Name', $name);
1551
1552
            if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
1553
                $attribute->setAttribute('NameFormat', $this->nameFormat);
1554
            }
1555
1556
            // make sure eduPersonTargetedID can be handled properly as a NameID
1557
            if ($name === Constants::EPTI_URN_MACE || $name === Constants::EPTI_URN_OID) {
1558
                foreach ($values as $eptiValue) {
1559
                    $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1560
                    $attribute->appendChild($attributeValue);
1561
                    if ($eptiValue instanceof XML\saml\NameID) {
1562
                        $eptiValue->toXML($attributeValue);
1563
                    } elseif ($eptiValue instanceof \DOMNodeList) {
1564
                        $node = $root->ownerDocument->importNode($eptiValue->item(0), true);
1565
                        $attributeValue->appendChild($node);
1566
                    } else {
1567
                        $attributeValue->textContent = $eptiValue;
1568
                    }
1569
                }
1570
1571
                continue;
1572
            }
1573
1574
            // get value type(s) for the current attribute
1575
            if (is_array($this->attributesValueTypes) && array_key_exists($name, $this->attributesValueTypes)) {
1576
                $valueTypes = $this->attributesValueTypes[$name];
1577
                if (is_array($valueTypes) && count($valueTypes) != count($values)) {
1578
                    throw new \Exception('Array of value types and array of values have different size for attribute '. var_export($name, true));
1579
                }
1580
            } else {
1581
                // if no type(s), default behaviour
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1582
                $valueTypes = null;
1583
            }
1584
1585
            $vidx = -1;
1586
            foreach ($values as $value) {
1587
                $vidx++;
1588
1589
                // try to get type from current types
1590
                $type = null;
1591
                if (!is_null($valueTypes)) {
1592
                    if (is_array($valueTypes)) {
1593
                        $type = $valueTypes[$vidx];
1594
                    } else {
1595
                        $type = $valueTypes;
1596
                    }
1597
                }
1598
1599
                // if no type get from types, use default behaviour
1600 View Code Duplication
                if (is_null($type)) {
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...
1601
                    if (is_string($value)) {
1602
                        $type = 'xs:string';
1603
                    } elseif (is_int($value)) {
1604
                        $type = 'xs:integer';
1605
                    } else {
1606
                        $type = null;
1607
                    }
1608
                }
1609
1610
                $attributeValue = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1611
                $attribute->appendChild($attributeValue);
1612
                if ($type !== null) {
1613
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type);
1614
                }
1615
                if (is_null($value)) {
1616
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:nil', 'true');
1617
                }
1618
1619 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...
1620
                    for ($i = 0; $i < $value->length; $i++) {
1621
                        $node = $document->importNode($value->item($i), true);
1622
                        $attributeValue->appendChild($node);
1623
                    }
1624
                } else {
1625
                    $attributeValue->appendChild($document->createTextNode($value));
1626
                }
1627
            }
1628
        }
1629
    }
1630
1631
1632
    /**
1633
     * Add an EncryptedAttribute Statement-node to the assertion.
1634
     *
1635
     * @param \DOMElement $root The assertion element we should add the Encrypted Attribute Statement to.
1636
     */
1637
    private function addEncryptedAttributeStatement(\DOMElement $root)
1638
    {
1639
        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...
1640
            return;
1641
        }
1642
1643
        $document = $root->ownerDocument;
1644
1645
        $attributeStatement = $document->createElementNS(Constants::NS_SAML, 'saml:AttributeStatement');
1646
        $root->appendChild($attributeStatement);
1647
1648
        foreach ($this->attributes as $name => $values) {
1649
            $document2 = DOMDocumentFactory::create();
1650
            $attribute = $document2->createElementNS(Constants::NS_SAML, 'saml:Attribute');
1651
            $attribute->setAttribute('Name', $name);
1652
            $document2->appendChild($attribute);
1653
1654
            if ($this->nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
1655
                $attribute->setAttribute('NameFormat', $this->nameFormat);
1656
            }
1657
1658
            foreach ($values as $value) {
1659 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...
1660
                    $type = 'xs:string';
1661
                } elseif (is_int($value)) {
1662
                    $type = 'xs:integer';
1663
                } else {
1664
                    $type = null;
1665
                }
1666
1667
                $attributeValue = $document2->createElementNS(Constants::NS_SAML, 'saml:AttributeValue');
1668
                $attribute->appendChild($attributeValue);
1669
                if ($type !== null) {
1670
                    $attributeValue->setAttributeNS(Constants::NS_XSI, 'xsi:type', $type);
1671
                }
1672
1673 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...
1674
                    for ($i = 0; $i < $value->length; $i++) {
1675
                        $node = $document2->importNode($value->item($i), true);
1676
                        $attributeValue->appendChild($node);
1677
                    }
1678
                } else {
1679
                    $attributeValue->appendChild($document2->createTextNode($value));
1680
                }
1681
            }
1682
            /*Once the attribute nodes are built, the are encrypted*/
1683
            $EncAssert = new XMLSecEnc();
1684
            $EncAssert->setNode($document2->documentElement);
1685
            $EncAssert->type = 'http://www.w3.org/2001/04/xmlenc#Element';
1686
            /*
1687
             * Attributes are encrypted with a session key and this one with
1688
             * $EncryptionKey
1689
             */
1690
            $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES256_CBC);
1691
            $symmetricKey->generateSessionKey();
1692
            $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...
1693
            $EncrNode = $EncAssert->encryptNode($symmetricKey);
1694
1695
            $EncAttribute = $document->createElementNS(Constants::NS_SAML, 'saml:EncryptedAttribute');
1696
            $attributeStatement->appendChild($EncAttribute);
1697
            $n = $document->importNode($EncrNode, true);
1698
            $EncAttribute->appendChild($n);
1699
        }
1700
    }
1701
}
1702