Completed
Push — master ( 1cea30...381bb7 )
by Thijs
08:24
created

Message::setIssueInstant()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 9.4285
1
<?php
2
3
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecurityKey;
6
use SAML2\Utilities\Temporal;
7
use SAML2\XML\samlp\Extensions;
8
9
/**
10
 * Base class for all SAML 2 messages.
11
 *
12
 * Implements what is common between the samlp:RequestAbstractType and
13
 * samlp:StatusResponseType element types.
14
 *
15
 *
16
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17
 */
18
abstract class Message implements SignedElement
19
{
20
    /**
21
     * Request extensions.
22
     *
23
     * @var array
24
     */
25
    protected $extensions;
26
27
    /**
28
     * The name of the root element of the DOM tree for the message.
29
     *
30
     * Used when creating a DOM tree from the message.
31
     *
32
     * @var string
33
     */
34
    private $tagName;
35
36
    /**
37
     * The identifier of this message.
38
     *
39
     * @var string
40
     */
41
    private $id;
42
43
    /**
44
     * The issue timestamp of this message, as an UNIX timestamp.
45
     *
46
     * @var int
47
     */
48
    private $issueInstant;
49
50
    /**
51
     * The destination URL of this message if it is known.
52
     *
53
     * @var string|null
54
     */
55
    private $destination;
56
57
    /**
58
     * The destination URL of this message if it is known.
59
     *
60
     * @var string|null
61
     */
62
    private $consent = Constants::CONSENT_UNSPECIFIED;
63
64
    /**
65
     * The entity id of the issuer of this message, or null if unknown.
66
     *
67
     * @var string|object|null
68
     */
69
    private $issuer;
70
71
    /**
72
     * The RelayState associated with this message.
73
     *
74
     * @var string|null
75
     */
76
    private $relayState;
77
78
    /**
79
     * The \DOMDocument we are currently building.
80
     *
81
     * This variable is used while generating XML from this message. It holds the
82
     * \DOMDocument of the XML we are generating.
83
     *
84
     * @var \DOMDocument
85
     */
86
    protected $document;
87
88
    /**
89
     * The private key we should use to sign the message.
90
     *
91
     * The private key can be null, in which case the message is sent unsigned.
92
     *
93
     * @var XMLSecurityKey|null
94
     */
95
    private $signatureKey;
96
97
    /**
98
     * @var bool
99
     */
100
    protected $messageContainedSignatureUponConstruction = false;
101
102
    /**
103
     * List of certificates that should be included in the message.
104
     *
105
     * @var array
106
     */
107
    private $certificates;
108
109
    /**
110
     * Available methods for validating this message.
111
     *
112
     * @var array
113
     */
114
    private $validators;
115
116
    /**
117
     * @var null|string
118
     */
119
    private $signatureMethod;
120
121
    /**
122
     * Initialize a message.
123
     *
124
     * This constructor takes an optional parameter with a \DOMElement. If this
125
     * parameter is given, the message will be initialized with data from that
126
     * XML element.
127
     *
128
     * If no XML element is given, the message is initialized with suitable
129
     * default values.
130
     *
131
     * @param string           $tagName The tag name of the root element
132
     * @param \DOMElement|null $xml     The input message
133
     *
134
     * @throws \Exception
135
     */
136
    protected function __construct($tagName, \DOMElement $xml = null)
137
    {
138
        assert('is_string($tagName)');
139
        $this->tagName = $tagName;
140
141
        $this->id = Utils::getContainer()->generateId();
142
        $this->issueInstant = Temporal::getTime();
143
        $this->certificates = array();
144
        $this->validators = array();
145
146
        if ($xml === null) {
147
            return;
148
        }
149
150
        if (!$xml->hasAttribute('ID')) {
151
            throw new \Exception('Missing ID attribute on SAML message.');
152
        }
153
        $this->id = $xml->getAttribute('ID');
154
155
        if ($xml->getAttribute('Version') !== '2.0') {
156
            /* Currently a very strict check. */
157
            throw new \Exception('Unsupported version: '.$xml->getAttribute('Version'));
158
        }
159
160
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
161
162
        if ($xml->hasAttribute('Destination')) {
163
            $this->destination = $xml->getAttribute('Destination');
164
        }
165
166
        if ($xml->hasAttribute('Consent')) {
167
            $this->consent = $xml->getAttribute('Consent');
168
        }
169
170
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
171
        if (!empty($issuer)) {
172
            $this->issuer = trim($issuer[0]->textContent);
173
        }
174
175
        /* Validate the signature element of the message. */
176
        try {
177
            /** @var null|\DOMAttr $signatureMethod */
178
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
179
180
            $sig = Utils::validateElement($xml);
181
182
            if ($sig !== false) {
183
                $this->messageContainedSignatureUponConstruction = true;
184
                $this->certificates = $sig['Certificates'];
185
                $this->validators[] = array(
186
                    'Function' => array('\SAML2\Utils', 'validateSignature'),
187
                    'Data' => $sig,
188
                );
189
                $this->signatureMethod = $signatureMethod[0]->value;
190
            }
191
        } catch (\Exception $e) {
192
            /* Ignore signature validation errors. */
193
        }
194
195
        $this->extensions = Extensions::getList($xml);
196
    }
197
198
    /**
199
     * Add a method for validating this message.
200
     *
201
     * This function is used by the HTTP-Redirect binding, to make it possible to
202
     * check the signature against the one included in the query string.
203
     *
204
     * @param callback $function The function which should be called
205
     * @param mixed    $data     The data that should be included as the first parameter to the function
206
     */
207
    public function addValidator($function, $data)
208
    {
209
        assert('is_callable($function)');
210
211
        $this->validators[] = array(
212
            'Function' => $function,
213
            'Data' => $data,
214
        );
215
    }
216
217
    /**
218
     * Validate this message against a public key.
219
     *
220
     * true is returned on success, false is returned if we don't have any
221
     * signature we can validate. An exception is thrown if the signature
222
     * validation fails.
223
     *
224
     * @param XMLSecurityKey $key The key we should check against
225
     *
226
     * @return bool true on success, false when we don't have a signature
227
     *
228
     * @throws \Exception
229
     */
230 View Code Duplication
    public function validate(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...
231
    {
232
        if (count($this->validators) === 0) {
233
            return false;
234
        }
235
236
        $exceptions = array();
237
238
        foreach ($this->validators as $validator) {
239
            $function = $validator['Function'];
240
            $data = $validator['Data'];
241
242
            try {
243
                call_user_func($function, $data, $key);
244
                /* We were able to validate the message with this validator. */
245
246
                return true;
247
            } catch (\Exception $e) {
248
                $exceptions[] = $e;
249
            }
250
        }
251
252
        /* No validators were able to validate the message. */
253
        throw $exceptions[0];
254
    }
255
256
    /**
257
     * Retrieve the identifier of this message.
258
     *
259
     * @return string The identifier of this message
260
     */
261
    public function getId()
262
    {
263
        return $this->id;
264
    }
265
266
    /**
267
     * Set the identifier of this message.
268
     *
269
     * @param string $id The new identifier of this message
270
     */
271
    public function setId($id)
272
    {
273
        assert('is_string($id)');
274
275
        $this->id = $id;
276
    }
277
278
    /**
279
     * Retrieve the issue timestamp of this message.
280
     *
281
     * @return int The issue timestamp of this message, as an UNIX timestamp
282
     */
283
    public function getIssueInstant()
284
    {
285
        return $this->issueInstant;
286
    }
287
288
    /**
289
     * Set the issue timestamp of this message.
290
     *
291
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp
292
     */
293
    public function setIssueInstant($issueInstant)
294
    {
295
        assert('is_int($issueInstant)');
296
297
        $this->issueInstant = $issueInstant;
298
    }
299
300
    /**
301
     * Retrieve the destination of this message.
302
     *
303
     * @return string|null The destination of this message, or NULL if no destination is given
304
     */
305
    public function getDestination()
306
    {
307
        return $this->destination;
308
    }
309
310
    /**
311
     * Set the destination of this message.
312
     *
313
     * @param string|null $destination The new destination of this message
314
     */
315
    public function setDestination($destination)
316
    {
317
        assert('is_string($destination) || is_null($destination)');
318
319
        $this->destination = $destination;
320
    }
321
322
    /**
323
     * Set the given consent for this message.
324
     *
325
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
326
     *
327
     * @see \SAML2\Constants
328
     *
329
     * @param string $consent
330
     */
331
    public function setConsent($consent)
332
    {
333
        assert('is_string($consent)');
334
335
        $this->consent = $consent;
336
    }
337
338
    /**
339
     * Set the given consent for this message.
340
     *
341
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
342
     *
343
     * @see \SAML2\Constants
344
     *
345
     * @return string Consent
346
     */
347
    public function getConsent()
348
    {
349
        return $this->consent;
350
    }
351
352
    /**
353
     * Retrieve the issuer if this message.
354
     *
355
     * @return string|object|null The issuer of this message, or NULL if no issuer is given
356
     */
357
    public function getIssuer()
358
    {
359
        if (is_string($this->issuer)) {
360
            return $this->issuer;
361
        } elseif (is_object($this->issuer)) {
362
            return $this->issuer->__toString();
363
        } else {
364
            return null;
365
        }
366
    }
367
368
    /**
369
     * Set the issuer of this message.
370
     *
371
     * @param string|object|null $issuer The new issuer of this message
372
     */
373
    public function setIssuer($issuer)
374
    {
375
        assert('is_string($issuer) || is_object($issuer) || is_null($issuer)');
376
377
        $this->issuer = $issuer;
378
    }
379
380
    /**
381
     * Query whether or not the message contained a signature at the root level when the object was constructed.
382
     *
383
     * @return bool
384
     */
385
    public function isMessageConstructedWithSignature()
386
    {
387
        return $this->messageContainedSignatureUponConstruction;
388
    }
389
390
    /**
391
     * Retrieve the RelayState associated with this message.
392
     *
393
     * @return string|null The RelayState, or NULL if no RelayState is given
394
     */
395
    public function getRelayState()
396
    {
397
        return $this->relayState;
398
    }
399
400
    /**
401
     * Set the RelayState associated with this message.
402
     *
403
     * @param string|null $relayState The new RelayState
404
     */
405
    public function setRelayState($relayState)
406
    {
407
        assert('is_string($relayState) || is_null($relayState)');
408
409
        $this->relayState = $relayState;
410
    }
411
412
    /**
413
     * Convert this message to an unsigned XML document.
414
     *
415
     * This method does not sign the resulting XML document.
416
     *
417
     * @return \DOMElement The root element of the DOM tree
418
     */
419
    public function toUnsignedXML()
420
    {
421
        $this->document = DOMDocumentFactory::create();
422
423
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:'.$this->tagName);
424
        $this->document->appendChild($root);
425
426
        /* Ugly hack to add another namespace declaration to the root element. */
427
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
428
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
429
430
        $root->setAttribute('ID', $this->id);
431
        $root->setAttribute('Version', '2.0');
432
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
433
434
        if ($this->destination !== null) {
435
            $root->setAttribute('Destination', $this->destination);
436
        }
437
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
438
            $root->setAttribute('Consent', $this->consent);
439
        }
440
441
        if ($this->issuer !== null) {
442
            if (is_string($this->issuer)) {
443
                Utils::addString($root, \SAML2_Const::NS_SAML, 'saml:Issuer', $this->issuer);
444
            } elseif (is_object($this->issuer)) {
445
                $this->setIssuerAttribute($root);
446
            }
447
        }
448
449
        if (!empty($this->extensions)) {
450
            Extensions::addList($root, $this->extensions);
451
        }
452
453
        return $root;
454
    }
455
456
    /**
457
     * Set attribute for Issuer if is an object.
458
     */
459
    private function setIssuerAttribute($root)
460
    {
461
        Utils::addString($root, \SAML2_Const::NS_SAML, 'saml:Issuer', $this->issuer->__toString());
462
        $issuer = \SAML2_Utils::xpQuery($root, './saml_assertion:Issuer');
463
        if ($this->issuer->getNameQualifier()) {
464
            $issuer[0]->setAttribute('NameQualifier', $this->issuer->getNameQualifier());
465
        }
466
        if ($this->issuer->getFormat()) {
467
            $issuer[0]->setAttribute('Format', $this->issuer->getFormat());
468
        }
469
        if ($this->issuer->getSPNameQualifier()) {
470
            $issuer[0]->setAttribute('SPNameQualifier', $this->issuer->getSPNameQualifier());
471
        }
472
        if ($this->issuer->getSPProvidedID()) {
473
            $issuer[0]->setAttribute('SPProvidedID', $this->issuer->getSPProvidedID());
474
        }
475
    }
476
477
    /**
478
     * Convert this message to a signed XML document.
479
     *
480
     * This method sign the resulting XML document if the private key for
481
     * the signature is set.
482
     *
483
     * @return \DOMElement The root element of the DOM tree
484
     */
485
    public function toSignedXML()
486
    {
487
        $root = $this->toUnsignedXML();
488
489
        if ($this->signatureKey === null) {
490
            /* We don't have a key to sign it with. */
491
492
            return $root;
493
        }
494
495
        /* Find the position we should insert the signature node at. */
496
        if ($this->issuer !== null) {
497
            /*
498
             * We have an issuer node. The signature node should come
499
             * after the issuer node.
500
             */
501
            $issuerNode = $root->firstChild;
502
            $insertBefore = $issuerNode->nextSibling;
503
        } else {
504
            /* No issuer node - the signature element should be the first element. */
505
            $insertBefore = $root->firstChild;
506
        }
507
508
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
509
510
        return $root;
511
    }
512
513
    /**
514
     * Retrieve the private key we should use to sign the message.
515
     *
516
     * @return XMLSecurityKey|null The key, or NULL if no key is specified
517
     */
518
    public function getSignatureKey()
519
    {
520
        return $this->signatureKey;
521
    }
522
523
    /**
524
     * Set the private key we should use to sign the message.
525
     *
526
     * If the key is null, the message will be sent unsigned.
527
     *
528
     * @param XMLSecurityKey|null $signatureKey
529
     */
530
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
531
    {
532
        $this->signatureKey = $signatureKey;
533
    }
534
535
    /**
536
     * Set the certificates that should be included in the message.
537
     *
538
     * The certificates should be strings with the PEM encoded data.
539
     *
540
     * @param array $certificates An array of certificates
541
     */
542
    public function setCertificates(array $certificates)
543
    {
544
        $this->certificates = $certificates;
545
    }
546
547
    /**
548
     * Retrieve the certificates that are included in the message.
549
     *
550
     * @return array An array of certificates
551
     */
552
    public function getCertificates()
553
    {
554
        return $this->certificates;
555
    }
556
557
    /**
558
     * Convert an XML element into a message.
559
     *
560
     * @param \DOMElement $xml The root XML element
561
     *
562
     * @return \SAML2\Message The message
563
     *
564
     * @throws \Exception
565
     */
566
    public static function fromXML(\DOMElement $xml)
567
    {
568 View Code Duplication
        if ($xml->namespaceURI !== Constants::NS_SAMLP) {
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...
569
            throw new \Exception('Unknown namespace of SAML message: '.var_export($xml->namespaceURI, true));
570
        }
571
572
        switch ($xml->localName) {
573
            case 'AttributeQuery':
574
                return new AttributeQuery($xml);
575
            case 'AuthnRequest':
576
                return new AuthnRequest($xml);
577
            case 'LogoutResponse':
578
                return new LogoutResponse($xml);
579
            case 'LogoutRequest':
580
                return new LogoutRequest($xml);
581
            case 'Response':
582
                return new Response($xml);
583
            case 'ArtifactResponse':
584
                return new ArtifactResponse($xml);
585
            case 'ArtifactResolve':
586
                return new ArtifactResolve($xml);
587
            default:
588
                throw new \Exception('Unknown SAML message: '.var_export($xml->localName, true));
589
        }
590
    }
591
592
    /**
593
     * Retrieve the Extensions.
594
     *
595
     * @return \SAML2\XML\samlp\Extensions
596
     */
597
    public function getExtensions()
598
    {
599
        return $this->extensions;
600
    }
601
602
    /**
603
     * Set the Extensions.
604
     *
605
     * @param array|null $extensions The Extensions
606
     */
607
    public function setExtensions($extensions)
608
    {
609
        assert('is_array($extensions) || is_null($extensions)');
610
611
        $this->extensions = $extensions;
0 ignored issues
show
Documentation Bug introduced by
It seems like $extensions can be null. However, the property $extensions is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

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

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
612
    }
613
614
    /**
615
     * @return null|string
616
     */
617
    public function getSignatureMethod()
618
    {
619
        return $this->signatureMethod;
620
    }
621
}
622