Passed
Pull Request — master (#226)
by Jaime Pérez
02:37
created

AbstractMessage::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 19
rs 9.9666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SAML2\XML\samlp;
6
7
use DOMElement;
8
use RobRichards\XMLSecLibs\XMLSecurityKey;
9
use SAML2\Constants;
10
use SAML2\Utilities\Temporal;
11
use SAML2\Utils;
12
use SAML2\XML\ExtendableElementTrait;
13
use SAML2\XML\saml\Issuer;
14
use SAML2\XML\SignedElementInterface;
15
use SAML2\XML\SignedElementTrait;
16
use Webmozart\Assert\Assert;
17
18
/**
19
 * Base class for all SAML 2 messages.
20
 *
21
 * Implements what is common between the samlp:RequestAbstractType and
22
 * samlp:StatusResponseType element types.
23
 */
24
abstract class AbstractMessage extends AbstractSamlpElement implements SignedElementInterface
25
{
26
    use ExtendableElementTrait;
27
    use SignedElementTrait {
28
        SignedElementTrait::validate as validateEnvelopedXmlSignature;
29
    }
30
31
    /**
32
     * The identifier of this message.
33
     *
34
     * @var string
35
     */
36
    protected $id;
37
38
    /**
39
     * The version of this message.
40
     *
41
     * @var string
42
     */
43
    protected $version;
44
45
    /**
46
     * The issue timestamp of this message, as an UNIX timestamp.
47
     *
48
     * @var int
49
     */
50
    protected $issueInstant;
51
52
    /**
53
     * The destination URL of this message if it is known.
54
     *
55
     * @var string|null
56
     */
57
    protected $destination = null;
58
59
    /**
60
     * The destination URL of this message if it is known.
61
     *
62
     * @var string|null
63
     */
64
    protected $consent;
65
66
    /**
67
     * The entity id of the issuer of this message, or null if unknown.
68
     *
69
     * @var \SAML2\XML\saml\Issuer|null
70
     */
71
    protected $issuer = null;
72
73
    /**
74
     * The RelayState associated with this message.
75
     *
76
     * @var string|null
77
     */
78
    protected $relayState = null;
79
80
    /**
81
     * The \DOMDocument we are currently building.
82
     *
83
     * This variable is used while generating XML from this message. It holds the
84
     * \DOMDocument of the XML we are generating.
85
     *
86
     * @var \DOMDocument
87
     */
88
    protected $document;
89
90
    /**
91
     * @var bool
92
     */
93
    protected $messageContainedSignatureUponConstruction = false;
94
95
    /**
96
     * Available methods for validating this message.
97
     *
98
     * @var array
99
     */
100
    private $validators = [];
101
102
    /**
103
     * @var null|string
104
     */
105
    private $signatureMethod = null;
106
107
108
    /**
109
     * Initialize a message.
110
     *
111
     * @param \SAML2\XML\saml\Issuer|null $issuer
112
     * @param string|null $id
113
     * @param string|null $version
114
     * @param int|null $issueInstant
115
     * @param string|null $destination
116
     * @param string|null $consent
117
     * @param \SAML2\XML\samlp\Extensions $extensions
118
     * @param string|null $relayState
119
     *
120
     * @throws \Exception
121
     */
122
    protected function __construct(
123
        ?Issuer $issuer = null,
124
        ?string $id = null,
125
        ?string $version = null,
126
        ?int $issueInstant = null,
127
        ?string $destination = null,
128
        ?string $consent = null,
129
        ?Extensions $extensions = null,
130
        ?string $relayState = null
131
    ) {
132
        $this->setIssuer($issuer);
133
        $this->setId($id);
134
        $this->setVersion($version);
135
        $this->setIssueInstant($issueInstant);
136
        $this->setDestination($destination);
137
        $this->setConsent($consent);
138
        $this->setExtensions($extensions);
139
        $this->setRelayState($relayState);
140
        $this->addValidator([$this, 'xmlSignatureValidatorWrapper'], []);
141
    }
142
143
144
    /**
145
     * Validate the signature element of a SAML message, and configure this object appropriately to perform the
146
     * signature verification afterwards.
147
     *
148
     * Please note this method does NOT verify the signature, it just validates the signature construction and prepares
149
     * this object to do the verification.
150
     *
151
     * @param \DOMElement $xml The SAML message whose signature we want to validate.
152
     * @return void
153
     */
154
    protected function validateSignature(DOMElement $xml): void
155
    {
156
        try {
157
            /** @var \DOMAttr[] $signatureMethod */
158
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
159
            if (empty($signatureMethod)) {
160
                throw new \Exception('No Algorithm specified in signature.');
161
            }
162
163
            $sig = Utils::validateElement($xml);
164
165
            if ($sig !== false) {
166
                $this->messageContainedSignatureUponConstruction = true;
167
                $this->certificates = $sig['Certificates'];
168
                $this->validators[] = [
169
                    'Function' => ['\SAML2\Utils', 'validateSignature'],
170
                    'Data' => $sig,
171
                ];
172
                $this->signatureMethod = $signatureMethod[0]->value;
173
            }
174
        } catch (\Exception $e) {
175
            // ignore signature validation errors
176
        }
177
    }
178
179
180
    /**
181
     * Add a method for validating this message.
182
     *
183
     * This function is used by the HTTP-Redirect binding, to make it possible to
184
     * check the signature against the one included in the query string.
185
     *
186
     * @param callable $function The function which should be called
187
     * @param mixed $data The data that should be included as the first parameter to the function
188
     * @return void
189
     */
190
    public function addValidator(callable $function, $data): void
191
    {
192
        $this->validators[] = [
193
            'Function' => $function,
194
            'Data' => $data,
195
        ];
196
    }
197
198
199
    /**
200
     * Validate this message against a public key.
201
     *
202
     * true is returned on success, false is returned if we don't have any
203
     * signature we can validate. An exception is thrown if the signature
204
     * validation fails.
205
     *
206
     * @param XMLSecurityKey $key The key we should check against
207
     * @throws \Exception
208
     * @return bool true on success, false when we don't have a signature
209
     */
210
    public function validate(XMLSecurityKey $key): bool
211
    {
212
        if (count($this->validators) === 0) {
213
            return false;
214
        }
215
216
        $exceptions = [];
217
218
        foreach ($this->validators as $validator) {
219
            $function = $validator['Function'];
220
            $data = $validator['Data'];
221
222
            try {
223
                call_user_func($function, $data, $key);
224
                /* We were able to validate the message with this validator. */
225
226
                return true;
227
            } catch (\Exception $e) {
228
                $exceptions[] = $e;
229
            }
230
        }
231
232
        /* No validators were able to validate the message. */
233
        throw array_pop($exceptions);
234
    }
235
236
237
    /**
238
     * Retrieve the identifier of this message.
239
     *
240
     * @return string The identifier of this message
241
     */
242
    public function getId(): string
243
    {
244
        return $this->id;
245
    }
246
247
248
    /**
249
     * Set the identifier of this message.
250
     *
251
     * @param string|null $id The new identifier of this message
252
     * @return void
253
     */
254
    private function setId(?string $id): void
255
    {
256
        if ($id === null) {
257
            $id = Utils::getContainer()->generateId();
258
        }
259
260
        $this->id = $id;
261
    }
262
263
264
    /**
265
     * Retrieve the version of this message.
266
     *
267
     * @return string The version of this message
268
     */
269
    public function getVersion(): string
270
    {
271
        return $this->version;
272
    }
273
274
275
    /**
276
     * Set the version of this message.
277
     *
278
     * @param string|null $id The version of this message
279
     * @return void
280
     */
281
    private function setVersion(?string $version): void
282
    {
283
        if ($version === null) {
284
            $version = '2.0';
285
        }
286
287
        Assert::same($version, '2.0');
288
        $this->version = $version;
289
    }
290
291
292
    /**
293
     * Retrieve the issue timestamp of this message.
294
     *
295
     * @return int The issue timestamp of this message, as an UNIX timestamp
296
     */
297
    public function getIssueInstant(): int
298
    {
299
        return $this->issueInstant;
300
    }
301
302
303
    /**
304
     * Set the issue timestamp of this message.
305
     *
306
     * @param int|null $issueInstant The new issue timestamp of this message, as an UNIX timestamp
307
     * @return void
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 destination of this message.
321
     *
322
     * @return string|null The destination of this message, or NULL if no destination is given
323
     */
324
    public function getDestination(): ?string
325
    {
326
        return $this->destination;
327
    }
328
329
330
    /**
331
     * Set the destination of this message.
332
     *
333
     * @param string|null $destination The new destination of this message
334
     * @return void
335
     */
336
    private function setDestination(string $destination = null): void
337
    {
338
        $this->destination = $destination;
339
    }
340
341
342
    /**
343
     * Get the given consent for this message.
344
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
345
     *
346
     * @see \SAML2\Constants
347
     * @return string|null Consent
348
     */
349
    public function getConsent(): ?string
350
    {
351
        return $this->consent;
352
    }
353
354
355
    /**
356
     * Set the given consent for this message.
357
     * Most likely (though not required) a value of urn:oasis:names:tc:SAML:2.0:consent.
358
     *
359
     * @see \SAML2\Constants
360
     * @param string|null $consent
361
     * @return void
362
     */
363
    private function setConsent(?string $consent): void
364
    {
365
        $this->consent = $consent;
366
    }
367
368
369
    /**
370
     * Retrieve the issuer if this message.
371
     *
372
     * @return \SAML2\XML\saml\Issuer|null The issuer of this message, or NULL if no issuer is given
373
     */
374
    public function getIssuer(): ?Issuer
375
    {
376
        return $this->issuer;
377
    }
378
379
380
    /**
381
     * Set the issuer of this message.
382
     *
383
     * @param \SAML2\XML\saml\Issuer|null $issuer The new issuer of this message
384
     * @return void
385
     */
386
    private function setIssuer(Issuer $issuer = null): void
387
    {
388
        $this->issuer = $issuer;
389
    }
390
391
392
    /**
393
     * Query whether or not the message contained a signature at the root level when the object was constructed.
394
     *
395
     * @return bool
396
     */
397
    public function isMessageConstructedWithSignature(): bool
398
    {
399
        return $this->messageContainedSignatureUponConstruction;
400
    }
401
402
403
    /**
404
     * Retrieve the RelayState associated with this message.
405
     *
406
     * @return string|null The RelayState, or NULL if no RelayState is given
407
     */
408
    public function getRelayState(): ?string
409
    {
410
        return $this->relayState;
411
    }
412
413
414
    /**
415
     * Set the RelayState associated with this message.
416
     *
417
     * @param string|null $relayState The new RelayState
418
     * @return void
419
     */
420
    public function setRelayState(string $relayState = null): void
421
    {
422
        $this->relayState = $relayState;
423
    }
424
425
426
    /**
427
     * Wrapper method over SignedElementTrait to use as a validator for enveloped XML signatures.
428
     *
429
     * @param array $_
430
     * @param XMLSecurityKey $key The key to use to verify the enveloped signature.
431
     *
432
     * @throws \Exception If there's no enveloped signature, or it fails to validate.
433
     */
434
    protected function xmlSignatureValidatorWrapper(array $_, XMLSecurityKey $key): void
435
    {
436
        if ($this->validateEnvelopedXmlSignature($key) === false) {
437
            throw new \Exception('No enveloped signature found');
438
        }
439
    }
440
441
442
    /**
443
     * Convert this message to an unsigned XML document.
444
     * This method does not sign the resulting XML document.
445
     *
446
     * @return \DOMElement The root element of the DOM tree
447
     */
448
    public function toXML(?DOMElement $parent = null): DOMElement
449
    {
450
        $root = $this->instantiateParentElement($parent);
451
452
        /* Ugly hack to add another namespace declaration to the root element. */
453
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
454
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
455
456
        $root->setAttribute('ID', $this->id);
457
        $root->setAttribute('Version', '2.0');
458
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
459
460
        if ($this->destination !== null) {
461
            $root->setAttribute('Destination', $this->destination);
462
        }
463
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
464
            $root->setAttribute('Consent', $this->consent);
465
        }
466
467
        if ($this->issuer !== null) {
468
            $this->issuer->toXML($root);
469
        }
470
471
        if ($this->Extensions !== null && !$this->Extensions->isEmptyElement()) {
472
            $this->Extensions->toXML($root);
473
        }
474
475
        return $root;
476
    }
477
}
478