Passed
Push — master ( 0a030b...e5091c )
by Tim
02:16
created

Assertion::setId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2\XML\saml;
6
7
use DOMElement;
8
use Exception;
9
use SimpleSAML\Assert\Assert;
10
use SimpleSAML\SAML2\Compat\ContainerSingleton;
11
use SimpleSAML\SAML2\Constants as C;
12
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
13
use SimpleSAML\SAML2\Utilities\Temporal;
14
use SimpleSAML\SAML2\Utils\XPath;
15
use SimpleSAML\XML\Chunk;
16
use SimpleSAML\XML\DOMDocumentFactory;
17
use SimpleSAML\XML\Exception\InvalidDOMElementException;
18
use SimpleSAML\XML\Exception\MissingElementException;
19
use SimpleSAML\XML\Exception\TooManyElementsException;
20
use SimpleSAML\XML\Utils as XMLUtils;
21
use SimpleSAML\XMLSecurity\Utils\Security as SecurityUtils;
22
use SimpleSAML\XMLSecurity\XML\ds\Signature;
23
use SimpleSAML\XMLSecurity\XML\EncryptableElementInterface;
24
use SimpleSAML\XMLSecurity\XML\EncryptableElementTrait;
25
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
26
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
27
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
28
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;
29
use SimpleSAML\XMLSecurity\Backend\EncryptionBackend;
30
31
use function array_filter;
32
use function array_merge;
33
use function array_pop;
34
use function array_values;
35
36
/**
37
 * Class representing a SAML 2 assertion.
38
 *
39
 * @package simplesamlphp/saml2
40
 */
41
final class Assertion extends AbstractSamlElement implements
42
    EncryptableElementInterface,
43
    SignableElementInterface,
44
    SignedElementInterface
45
{
46
    use EncryptableElementTrait;
47
    use SignableElementTrait;
48
    use SignedElementTrait;
49
50
    /**
51
     * The identifier of this assertion.
52
     *
53
     * @var string
54
     */
55
    protected string $id;
56
57
    /**
58
     * The issue timestamp of this assertion, as an UNIX timestamp.
59
     *
60
     * @var int
61
     */
62
    protected int $issueInstant;
63
64
    /**
65
     * The issuer of this assertion.
66
     *
67
     * If the issuer's format is \SAML2\Constants::NAMEID_ENTITY, this property will just take the issuer's string
68
     * value.
69
     *
70
     * @var \SimpleSAML\SAML2\XML\saml\Issuer
71
     */
72
    protected Issuer $issuer;
73
74
    /**
75
     * The subject of this assertion
76
     *
77
     * @var \SimpleSAML\SAML2\XML\saml\Subject|null
78
     */
79
    protected ?Subject $subject;
80
81
    /**
82
     * The subject of this assertion
83
     *
84
     * If the NameId is null, no subject was included in the assertion.
85
     *
86
     * @var \SimpleSAML\SAML2\XML\saml\NameID|null
87
     */
88
    protected ?NameID $nameId = null;
89
90
    /**
91
     * The encrypted NameId of the subject.
92
     *
93
     * If this is not null, the NameId needs decryption before it can be accessed.
94
     *
95
     * @var \DOMElement|null
96
     */
97
    protected ?DOMElement $encryptedNameId = null;
98
99
    /**
100
     * The statements made by this assertion.
101
     *
102
     * @var \SimpleSAML\SAML2\XML\saml\AbstractStatementType[]
103
     */
104
    protected array $statements = [];
105
106
    /**
107
     * The attributes, as an associative array, indexed by attribute name
108
     *
109
     * To ease handling, all attribute values are represented as an array of values, also for values with a multiplicity
110
     * of single. There are 5 possible variants of datatypes for the values: a string, an integer, an array, a
111
     * DOMNodeList or a \SimpleSAML\SAML2\XML\saml\NameID object.
112
     *
113
     * If the attribute is an eduPersonTargetedID, the values will be SAML2\XML\saml\NameID objects.
114
     * If the attribute value has an type-definition (xsi:string or xsi:int), the values will be of that type.
115
     * If the attribute value contains a nested XML structure, the values will be a DOMNodeList
116
     * In all other cases the values are treated as strings
117
     *
118
     * **WARNING** a DOMNodeList cannot be serialized without data-loss and should be handled explicitly
119
     *
120
     * @var array multi-dimensional array of \DOMNodeList|\SimpleSAML\SAML2\XML\saml\NameID|string|int|array
121
     */
122
    protected array $attributes = [];
123
124
    /**
125
     * The SubjectConfirmation elements of the Subject in the assertion.
126
     *
127
     * @var \SimpleSAML\SAML2\XML\saml\SubjectConfirmation[]
128
     */
129
    protected array $SubjectConfirmation = [];
130
131
    /**
132
     * @var bool
133
     */
134
    protected bool $wasSignedAtConstruction = false;
135
136
    /**
137
     * @var \SimpleSAML\SAML2\XML\saml\Conditions|null
138
     */
139
    protected ?Conditions $conditions;
140
141
142
    /**
143
     * Assertion constructor.
144
     *
145
     * @param \SimpleSAML\SAML2\XML\saml\Issuer $issuer
146
     * @param string|null $id
147
     * @param int|null $issueInstant
148
     * @param \SimpleSAML\SAML2\XML\saml\Subject|null $subject
149
     * @param \SimpleSAML\SAML2\XML\saml\Conditions|null $conditions
150
     * @param \SimpleSAML\SAML2\XML\saml\AbstractStatementType[] $statements
151
     */
152
    public function __construct(
153
        Issuer $issuer,
154
        ?string $id = null,
155
        ?int $issueInstant = null,
156
        ?Subject $subject = null,
157
        ?Conditions $conditions = null,
158
        array $statements = []
159
    ) {
160
        $this->dataType = C::XMLENC_ELEMENT;
161
162
        Assert::true(
163
            $subject || !empty($statements),
164
            "Either a <saml:Subject> or some statement must be present in a <saml:Assertion>"
165
        );
166
        $this->setIssuer($issuer);
167
        $this->setId($id);
168
        $this->setIssueInstant($issueInstant);
169
        $this->setSubject($subject);
170
        $this->setConditions($conditions);
171
        $this->setStatements($statements);
172
    }
173
174
175
    /**
176
     * Collect the value of the subject
177
     *
178
     * @return \SimpleSAML\SAML2\XML\saml\Subject|null
179
     */
180
    public function getSubject(): ?Subject
181
    {
182
        return $this->subject;
183
    }
184
185
186
    /**
187
     * Set the value of the subject-property
188
     *
189
     * @param \SimpleSAML\SAML2\XML\saml\Subject|null $subject
190
     */
191
    protected function setSubject(?Subject $subject): void
192
    {
193
        $this->subject = $subject;
194
    }
195
196
197
    /**
198
     * Collect the value of the conditions-property
199
     *
200
     * @return \SimpleSAML\SAML2\XML\saml\Conditions|null
201
     */
202
    public function getConditions(): ?Conditions
203
    {
204
        return $this->conditions;
205
    }
206
207
208
    /**
209
     * Set the value of the conditions-property
210
     *
211
     * @param \SimpleSAML\SAML2\XML\saml\Conditions|null $conditions
212
     */
213
    protected function setConditions(?Conditions $conditions): void
214
    {
215
        $this->conditions = $conditions;
216
    }
217
218
219
    /**
220
     * @return \SimpleSAML\SAML2\XML\saml\AttributeStatement[]
221
     */
222
    public function getAttributeStatements(): array
223
    {
224
        return array_values(array_filter($this->statements, function ($statement) {
225
            return $statement instanceof AttributeStatement;
226
        }));
227
    }
228
229
230
    /**
231
     * @return \SimpleSAML\SAML2\XML\saml\AuthnStatement[]
232
     */
233
    public function getAuthnStatements(): array
234
    {
235
        return array_values(array_filter($this->statements, function ($statement) {
236
            return $statement instanceof AuthnStatement;
237
        }));
238
    }
239
240
241
    /**
242
     * @return \SimpleSAML\SAML2\XML\saml\AbstractStatement[]
243
     */
244
    public function getStatements(): array
245
    {
246
        return array_values(array_filter($this->statements, function ($statement) {
247
            return $statement instanceof AbstractStatement;
248
        }));
249
    }
250
251
252
    /**
253
     * Set the statements in this assertion
254
     *
255
     * @param \SimpleSAML\SAML2\XML\saml\AbstractStatementType[] $statements
256
     */
257
    protected function setStatements(array $statements): void
258
    {
259
        Assert::allIsInstanceOf($statements, AbstractStatementType::class);
260
261
        $this->statements = $statements;
262
    }
263
264
265
    /**
266
     * Retrieve the identifier of this assertion.
267
     *
268
     * @return string The identifier of this assertion.
269
     */
270
    public function getId(): string
271
    {
272
        return $this->id;
273
    }
274
275
276
    /**
277
     * Set the identifier of this assertion.
278
     *
279
     * @param string|null $id The new identifier of this assertion.
280
     */
281
    private function setId(?string $id): void
282
    {
283
        Assert::nullOrNotWhitespaceOnly($id);
284
285
        if ($id === null) {
286
            $container = ContainerSingleton::getInstance();
287
            $id = $container->generateId();
288
        }
289
        $this->id = $id;
290
    }
291
292
293
    /**
294
     * Retrieve the issue timestamp of this assertion.
295
     *
296
     * @return int The issue timestamp of this assertion, as an UNIX timestamp.
297
     */
298
    public function getIssueInstant(): int
299
    {
300
        return $this->issueInstant;
301
    }
302
303
304
    /**
305
     * Set the issue timestamp of this assertion.
306
     *
307
     * @param int|null $issueInstant The new issue timestamp of this assertion, as an UNIX timestamp.
308
     */
309
    private function setIssueInstant(?int $issueInstant): void
310
    {
311
        if ($issueInstant === null) {
312
            $issueInstant = Temporal::getTime();
313
        }
314
315
        $this->issueInstant = $issueInstant;
316
    }
317
318
319
    /**
320
     * Retrieve the issuer if this assertion.
321
     *
322
     * @return \SimpleSAML\SAML2\XML\saml\Issuer The issuer of this assertion.
323
     */
324
    public function getIssuer(): Issuer
325
    {
326
        return $this->issuer;
327
    }
328
329
330
    /**
331
     * Set the issuer of this message.
332
     *
333
     * @param \SimpleSAML\SAML2\XML\saml\Issuer $issuer The new issuer of this assertion.
334
     */
335
    private function setIssuer(Issuer $issuer): void
336
    {
337
        $this->issuer = $issuer;
338
    }
339
340
341
    /**
342
     * Retrieve the SubjectConfirmation elements we have in our Subject element.
343
     *
344
     * @return array Array of \SimpleSAML\SAML2\XML\saml\SubjectConfirmation elements.
345
     */
346
    public function getSubjectConfirmation(): array
347
    {
348
        return $this->SubjectConfirmation;
349
    }
350
351
352
    /**
353
     * @return bool
354
     */
355
    public function wasSignedAtConstruction(): bool
356
    {
357
        return $this->wasSignedAtConstruction;
358
    }
359
360
361
    /**
362
     * Get the XML element.
363
     *
364
     * @return \DOMElement
365
     */
366
    public function getXML(): DOMElement
367
    {
368
        return $this->xml;
369
    }
370
371
372
    /**
373
     * Set the XML element.
374
     *
375
     * @param \DOMElement $xml
376
     */
377
    private function setXML(DOMElement $xml): void
378
    {
379
        $this->xml = $xml;
0 ignored issues
show
Bug Best Practice introduced by
The property xml does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
380
    }
381
382
383
    /**
384
     * @return \DOMElement
385
     */
386
    protected function getOriginalXML(): DOMElement
387
    {
388
        return $this->xml ?? $this->toUnsignedXML();
389
    }
390
391
392
    public function getBlacklistedAlgorithms(): ?array
393
    {
394
        $container = ContainerSingleton::getInstance();
395
        return $container->getBlacklistedEncryptionAlgorithms();
396
    }
397
398
399
    public function getEncryptionBackend(): ?EncryptionBackend
400
    {
401
        // return the encryption backend you want to use,
402
        // or null if you are fine with the default
403
        return null;
404
    }
405
406
407
    /**
408
     * Convert XML into an Assertion
409
     *
410
     * @param \DOMElement $xml The XML element we should load
411
     * @return \SimpleSAML\SAML2\XML\saml\Assertion
412
     *
413
     * @throws \SimpleSAML\Assert\AssertionFailedException if assertions are false
414
     * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
415
     *   if the qualified name of the supplied element is wrong
416
     * @throws \SimpleSAML\XML\Exception\MissingAttributeException
417
     *   if the supplied element is missing one of the mandatory attributes
418
     * @throws \SimpleSAML\XML\Exception\MissingElementException if one of the mandatory child-elements is missing
419
     * @throws \SimpleSAML\XML\Exception\TooManyElementsException if too many child-elements of a type are specified
420
     * @throws \Exception
421
     */
422
    public static function fromXML(DOMElement $xml): static
423
    {
424
        Assert::same($xml->localName, 'Assertion', InvalidDOMElementException::class);
425
        Assert::same($xml->namespaceURI, Assertion::NS, InvalidDOMElementException::class);
426
        Assert::same(self::getAttribute($xml, 'Version'), '2.0', 'Unsupported version: %s');
427
428
        $issueInstant = self::getAttribute($xml, 'IssueInstant');
429
        // Strip sub-seconds - See paragraph 1.3.3 of SAML core specifications
430
        $issueInstant = preg_replace('/([.][0-9]+Z)$/', 'Z', $issueInstant, 1);
431
432
        Assert::validDateTimeZulu($issueInstant, ProtocolViolationException::class);
433
        $issueInstant = XMLUtils::xsDateTimeToTimestamp($issueInstant);
434
435
        $issuer = Issuer::getChildrenOfClass($xml);
436
        Assert::minCount($issuer, 1, 'Missing <saml:Issuer> in assertion.', MissingElementException::class);
437
        Assert::maxCount($issuer, 1, 'More than one <saml:Issuer> in assertion.', TooManyElementsException::class);
438
439
        $subject = Subject::getChildrenOfClass($xml);
440
        Assert::maxCount(
441
            $subject,
442
            1,
443
            'More than one <saml:Subject> in <saml:Assertion>',
444
            TooManyElementsException::class,
445
        );
446
447
        $conditions = Conditions::getChildrenOfClass($xml);
448
        Assert::maxCount(
449
            $conditions,
450
            1,
451
            'More than one <saml:Conditions> in <saml:Assertion>.',
452
            TooManyElementsException::class,
453
        );
454
455
        $signature = Signature::getChildrenOfClass($xml);
456
        Assert::maxCount($signature, 1, 'Only one <ds:Signature> element is allowed.', TooManyElementsException::class);
457
458
        $authnStatement = AuthnStatement::getChildrenOfClass($xml);
459
        $attrStatement = AttributeStatement::getChildrenOfClass($xml);
460
        $statements = AbstractStatement::getChildrenOfClass($xml);
461
462
        $assertion = new static(
463
            array_pop($issuer),
464
            self::getAttribute($xml, 'ID'),
465
            $issueInstant,
466
            array_pop($subject),
467
            array_pop($conditions),
468
            array_merge($authnStatement, $attrStatement, $statements)
469
        );
470
471
        if (!empty($signature)) {
472
            $assertion->setSignature($signature[0]);
473
            $assertion->wasSignedAtConstruction = true;
474
            $assertion->setXML($xml);
475
        }
476
477
        return $assertion;
478
    }
479
480
481
    /**
482
     * Convert this assertion to an unsigned XML document.
483
     * This method does not sign the resulting XML document.
484
     *
485
     * @return \DOMElement The root element of the DOM tree
486
     */
487
    protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
488
    {
489
        $e = $this->instantiateParentElement($parent);
490
491
        $e->setAttribute('Version', '2.0');
492
        $e->setAttribute('ID', $this->id);
493
        $e->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
494
495
        $this->issuer->toXML($e);
496
        $this->subject?->toXML($e);
497
        $this->conditions?->toXML($e);
498
499
        foreach ($this->statements as $statement) {
500
            $statement->toXML($e);
501
        }
502
503
        return $e;
504
    }
505
506
507
    /**
508
     * Convert this assertion to a signed XML element, if a signer was set.
509
     *
510
     * @param \DOMElement|null $parent The DOM node the assertion should be created in.
511
     *
512
     * @return \DOMElement This assertion.
513
     * @throws \Exception
514
     */
515
    public function toXML(DOMElement $parent = null): DOMElement
516
    {
517
        if ($this->isSigned() === true && $this->signer === null) {
518
            // We already have a signed document and no signer was set to re-sign it
519
            if ($parent === null) {
520
                return $this->xml;
521
            }
522
523
            $node = $parent->ownerDocument?->importNode($this->xml, true);
524
            $parent->appendChild($node);
525
            return $parent;
526
        }
527
528
        $e = $this->toUnsignedXML($parent);
529
530
        if ($this->signer !== null) {
531
            $signedXML = $this->doSign($e);
532
533
            // Test for an Issuer
534
            $messageElements = XPath::xpQuery($signedXML, './saml_assertion:Issuer', XPath::getXPath($signedXML));
535
            $issuer = array_pop($messageElements);
536
537
            $signedXML->insertBefore($this->signature?->toXML($signedXML), $issuer->nextSibling);
538
            return $signedXML;
539
        }
540
541
        return $e;
542
    }
543
}
544