Assertion::getId()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
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 DateTimeImmutable;
8
use DOMElement;
9
use SimpleSAML\SAML2\Assert\Assert;
10
use SimpleSAML\SAML2\Constants as C;
11
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
12
use SimpleSAML\SAML2\Utils\XPath;
13
use SimpleSAML\SAML2\XML\EncryptableElementTrait;
14
use SimpleSAML\SAML2\XML\SignableElementTrait;
15
use SimpleSAML\SAML2\XML\SignedElementTrait;
16
use SimpleSAML\XML\Exception\InvalidDOMElementException;
17
use SimpleSAML\XML\Exception\MissingElementException;
18
use SimpleSAML\XML\Exception\TooManyElementsException;
19
use SimpleSAML\XML\SchemaValidatableElementInterface;
20
use SimpleSAML\XML\SchemaValidatableElementTrait;
21
use SimpleSAML\XML\Utils\Random as RandomUtils;
22
use SimpleSAML\XMLSecurity\Backend\EncryptionBackend;
23
use SimpleSAML\XMLSecurity\XML\ds\Signature;
24
use SimpleSAML\XMLSecurity\XML\EncryptableElementInterface;
25
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
26
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
27
28
use function array_filter;
29
use function array_merge;
30
use function array_pop;
31
use function array_values;
32
33
/**
34
 * Class representing a SAML 2 assertion.
35
 *
36
 * @package simplesamlphp/saml2
37
 */
38
final class Assertion extends AbstractSamlElement implements
39
    EncryptableElementInterface,
40
    SchemaValidatableElementInterface,
41
    SignableElementInterface,
42
    SignedElementInterface
43
{
44
    use EncryptableElementTrait {
0 ignored issues
show
Bug introduced by
The trait SimpleSAML\SAML2\XML\EncryptableElementTrait requires the property $ownerDocument which is not provided by SimpleSAML\SAML2\XML\saml\Assertion.
Loading history...
45
        EncryptableElementTrait::getBlacklistedAlgorithms insteadof SignedElementTrait;
46
        EncryptableElementTrait::getBlacklistedAlgorithms insteadof SignableElementTrait;
47
    }
48
    use SchemaValidatableElementTrait;
49
    use SignableElementTrait;
0 ignored issues
show
introduced by
The trait SimpleSAML\SAML2\XML\SignableElementTrait requires some properties which are not provided by SimpleSAML\SAML2\XML\saml\Assertion: $ownerDocument, $documentElement
Loading history...
50
    use SignedElementTrait;
0 ignored issues
show
introduced by
The trait SimpleSAML\SAML2\XML\SignedElementTrait requires some properties which are not provided by SimpleSAML\SAML2\XML\saml\Assertion: $ownerDocument, $documentElement
Loading history...
51
52
53
    /**
54
     * @var bool
55
     */
56
    protected bool $wasSignedAtConstruction = false;
57
58
    /**
59
     * The original signed XML
60
     *
61
     * @var \DOMElement
62
     */
63
    protected DOMElement $xml;
64
65
66
    /**
67
     * Assertion constructor.
68
     *
69
     * @param \SimpleSAML\SAML2\XML\saml\Issuer $issuer
70
     * @param string|null $id
71
     * @param \DateTimeImmutable $issueInstant
72
     * @param \SimpleSAML\SAML2\XML\saml\Subject|null $subject
73
     * @param \SimpleSAML\SAML2\XML\saml\Conditions|null $conditions
74
     * @param \SimpleSAML\SAML2\XML\saml\AbstractStatementType[] $statements
75
     */
76
    public function __construct(
77
        protected Issuer $issuer,
78
        protected DateTimeImmutable $issueInstant,
79
        protected ?string $id = null,
80
        protected ?Subject $subject = null,
81
        protected ?Conditions $conditions = null,
82
        protected array $statements = [],
83
    ) {
84
        Assert::same($issueInstant->getTimeZone()->getName(), 'Z', ProtocolViolationException::class);
85
        Assert::nullOrValidNCName($id); // Covers the empty string
86
        Assert::true(
87
            $subject || !empty($statements),
88
            "Either a <saml:Subject> or some statement must be present in a <saml:Assertion>",
89
        );
90
        Assert::maxCount($statements, C::UNBOUNDED_LIMIT);
91
        Assert::allIsInstanceOf($statements, AbstractStatementType::class);
92
        Assert::nullOrNotWhitespaceOnly($id);
93
    }
94
95
96
    /**
97
     * Collect the value of the subject
98
     *
99
     * @return \SimpleSAML\SAML2\XML\saml\Subject|null
100
     */
101
    public function getSubject(): ?Subject
102
    {
103
        return $this->subject;
104
    }
105
106
107
    /**
108
     * Collect the value of the conditions-property
109
     *
110
     * @return \SimpleSAML\SAML2\XML\saml\Conditions|null
111
     */
112
    public function getConditions(): ?Conditions
113
    {
114
        return $this->conditions;
115
    }
116
117
118
    /**
119
     * @return \SimpleSAML\SAML2\XML\saml\AttributeStatement[]
120
     */
121
    public function getAttributeStatements(): array
122
    {
123
        return array_values(array_filter($this->statements, function ($statement) {
124
            return $statement instanceof AttributeStatement;
125
        }));
126
    }
127
128
129
    /**
130
     * @return \SimpleSAML\SAML2\XML\saml\AuthnStatement[]
131
     */
132
    public function getAuthnStatements(): array
133
    {
134
        return array_values(array_filter($this->statements, function ($statement) {
135
            return $statement instanceof AuthnStatement;
136
        }));
137
    }
138
139
140
    /**
141
     * @return \SimpleSAML\SAML2\XML\saml\AbstractStatement[]
142
     */
143
    public function getStatements(): array
144
    {
145
        return array_values(array_filter($this->statements, function ($statement) {
146
            return $statement instanceof AbstractStatement;
147
        }));
148
    }
149
150
151
    /**
152
     * Retrieve the identifier of this assertion.
153
     *
154
     * @return string The identifier of this assertion.
155
     */
156
    public function getId(): string
157
    {
158
        if ($this->id === null) {
159
            return (new RandomUtils())->generateId();
160
        }
161
162
        return $this->id;
163
    }
164
165
166
    /**
167
     * Retrieve the issue timestamp of this assertion.
168
     *
169
     * @return \DateTimeImmutable The issue timestamp of this assertion, as an UNIX timestamp.
170
     */
171
    public function getIssueInstant(): DateTimeImmutable
172
    {
173
        return $this->issueInstant;
174
    }
175
176
177
    /**
178
     * Retrieve the issuer if this assertion.
179
     *
180
     * @return \SimpleSAML\SAML2\XML\saml\Issuer The issuer of this assertion.
181
     */
182
    public function getIssuer(): Issuer
183
    {
184
        return $this->issuer;
185
    }
186
187
188
    /**
189
     * @return bool
190
     */
191
    public function wasSignedAtConstruction(): bool
192
    {
193
        return $this->wasSignedAtConstruction;
194
    }
195
196
197
    /**
198
     * Get the XML element.
199
     *
200
     * @return \DOMElement
201
     */
202
    public function getXML(): DOMElement
203
    {
204
        return $this->xml;
205
    }
206
207
208
    /**
209
     * Set the XML element.
210
     *
211
     * @param \DOMElement $xml
212
     */
213
    private function setXML(DOMElement $xml): void
214
    {
215
        $this->xml = $xml;
216
    }
217
218
219
    /**
220
     * @return \DOMElement
221
     */
222
    protected function getOriginalXML(): DOMElement
223
    {
224
        return $this->xml ?? $this->toUnsignedXML();
225
    }
226
227
228
    public function getEncryptionBackend(): ?EncryptionBackend
229
    {
230
        // return the encryption backend you want to use,
231
        // or null if you are fine with the default
232
        return null;
233
    }
234
235
236
    /**
237
     * Convert XML into an Assertion
238
     *
239
     * @param \DOMElement $xml The XML element we should load
240
     * @return static
241
     *
242
     * @throws \SimpleSAML\Assert\AssertionFailedException if assertions are false
243
     * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
244
     *   if the qualified name of the supplied element is wrong
245
     * @throws \SimpleSAML\XML\Exception\MissingAttributeException
246
     *   if the supplied element is missing one of the mandatory attributes
247
     * @throws \SimpleSAML\XML\Exception\MissingElementException if one of the mandatory child-elements is missing
248
     * @throws \SimpleSAML\XML\Exception\TooManyElementsException if too many child-elements of a type are specified
249
     * @throws \Exception
250
     */
251
    public static function fromXML(DOMElement $xml): static
252
    {
253
        Assert::same($xml->localName, 'Assertion', InvalidDOMElementException::class);
254
        Assert::same($xml->namespaceURI, Assertion::NS, InvalidDOMElementException::class);
255
        Assert::same(self::getAttribute($xml, 'Version'), '2.0', 'Unsupported version: %s');
256
257
        $id = self::getAttribute($xml, 'ID');
258
        Assert::validNCName($id); // Covers the empty string
259
260
        $issueInstant = self::getAttribute($xml, 'IssueInstant');
261
        // Strip sub-seconds - See paragraph 1.3.3 of SAML core specifications
262
        $issueInstant = preg_replace('/([.][0-9]+Z)$/', 'Z', $issueInstant, 1);
263
264
        Assert::validDateTime($issueInstant, ProtocolViolationException::class);
265
        $issueInstant = new DateTimeImmutable($issueInstant);
266
267
        $issuer = Issuer::getChildrenOfClass($xml);
268
        Assert::minCount($issuer, 1, 'Missing <saml:Issuer> in assertion.', MissingElementException::class);
269
        Assert::maxCount($issuer, 1, 'More than one <saml:Issuer> in assertion.', TooManyElementsException::class);
270
271
        $subject = Subject::getChildrenOfClass($xml);
272
        Assert::maxCount(
273
            $subject,
274
            1,
275
            'More than one <saml:Subject> in <saml:Assertion>',
276
            TooManyElementsException::class,
277
        );
278
279
        $conditions = Conditions::getChildrenOfClass($xml);
280
        Assert::maxCount(
281
            $conditions,
282
            1,
283
            'More than one <saml:Conditions> in <saml:Assertion>.',
284
            TooManyElementsException::class,
285
        );
286
287
        $signature = Signature::getChildrenOfClass($xml);
288
        Assert::maxCount($signature, 1, 'Only one <ds:Signature> element is allowed.', TooManyElementsException::class);
289
290
        $authnStatement = AuthnStatement::getChildrenOfClass($xml);
291
        $attrStatement = AttributeStatement::getChildrenOfClass($xml);
292
        $statements = AbstractStatement::getChildrenOfClass($xml);
293
294
        $assertion = new static(
295
            array_pop($issuer),
296
            $issueInstant,
297
            $id,
298
            array_pop($subject),
299
            array_pop($conditions),
300
            array_merge($authnStatement, $attrStatement, $statements),
301
        );
302
303
        if (!empty($signature)) {
304
            $assertion->setSignature($signature[0]);
305
            $assertion->wasSignedAtConstruction = true;
306
            $assertion->setXML($xml);
307
        }
308
309
        return $assertion;
310
    }
311
312
313
    /**
314
     * Convert this assertion to an unsigned XML document.
315
     * This method does not sign the resulting XML document.
316
     *
317
     * @return \DOMElement The root element of the DOM tree
318
     */
319
    protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
320
    {
321
        $e = $this->instantiateParentElement($parent);
322
323
        $e->setAttribute('Version', '2.0');
324
        $e->setAttribute('ID', $this->getId());
325
        $e->setAttribute('IssueInstant', $this->getIssueInstant()->format(C::DATETIME_FORMAT));
326
327
        $this->getIssuer()->toXML($e);
328
        $this->getSubject()?->toXML($e);
329
        $this->getConditions()?->toXML($e);
330
331
        foreach ($this->statements as $statement) {
332
            $statement->toXML($e);
333
        }
334
335
        return $e;
336
    }
337
338
339
    /**
340
     * Convert this assertion to a signed XML element, if a signer was set.
341
     *
342
     * @param \DOMElement|null $parent The DOM node the assertion should be created in.
343
     *
344
     * @return \DOMElement This assertion.
345
     * @throws \Exception
346
     */
347
    public function toXML(?DOMElement $parent = null): DOMElement
348
    {
349
        if ($this->isSigned() === true && $this->signer === null) {
350
            // We already have a signed document and no signer was set to re-sign it
351
            if ($parent === null) {
352
                return $this->getXML();
353
            }
354
355
            $node = $parent->ownerDocument?->importNode($this->getXML(), true);
356
            $parent->appendChild($node);
357
            return $parent;
358
        }
359
360
        $e = $this->toUnsignedXML($parent);
361
362
        if ($this->signer !== null) {
363
            $signedXML = $this->doSign($e);
364
365
            // Test for an Issuer
366
            $messageElements = XPath::xpQuery($signedXML, './saml_assertion:Issuer', XPath::getXPath($signedXML));
367
            $issuer = array_pop($messageElements);
368
369
            $signedXML->insertBefore($this->signature?->toXML($signedXML), $issuer->nextSibling);
370
            return $signedXML;
371
        }
372
373
        return $e;
374
    }
375
}
376