AuthnResponse   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 483
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 57
eloc 192
dl 0
loc 483
rs 5.04
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
B validate() 0 45 7
A getRelayState() 0 3 1
C getAttributes() 0 73 15
A getIssuer() 0 9 2
A doXPathQuery() 0 15 2
A setRelayState() 0 3 1
B checkDateConditions() 0 18 7
A getSessionIndex() 0 10 2
A encAttribute() 0 29 6
B generate() 0 86 5
A setMessageValidated() 0 3 1
A setXML() 0 6 2
A getNameID() 0 13 2
A isNodeValidated() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like AuthnResponse often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthnResponse, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Shib13;
6
7
use DOMDocument;
8
use DOMNode;
9
use DOMNodeList;
10
use DOMXPath;
11
use Exception;
12
use SimpleSAML\Assert\Assert;
13
use SimpleSAML\Configuration;
14
use SimpleSAML\Error;
15
use SimpleSAML\Metadata\MetaDataStorageHandler;
16
use SimpleSAML\Utils;
17
use SimpleSAML\XML\DOMDocumentFactory;
18
use SimpleSAML\XML\Utils as XMLUtils;
19
use SimpleSAML\XML\Utils\Random;
20
use SimpleSAML\XML\Validator;
21
use SimpleXMLElement;
22
23
/**
24
 * A Shibboleth 1.3 authentication response.
25
 *
26
 * @package SimpleSAMLphp
27
 */
28
class AuthnResponse
29
{
30
    /**
31
     * @var \SimpleSAML\XML\Validator|null This variable contains an XML validator for this message.
32
     */
33
    private ?Validator $validator = null;
34
35
    /**
36
     * @var bool Whether this response was validated by some external means (e.g. SSL).
37
     */
38
    private bool $messageValidated = false;
39
40
    /** @var string */
41
    public const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol';
42
43
    /** @var string */
44
    public const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion';
45
46
    /**
47
     * @var \DOMDocument|null The DOMDocument which represents this message.
48
     */
49
    private ?DOMDocument $dom = null;
50
51
    /**
52
     * @var string|null The relaystate which is associated with this response.
53
     */
54
    private ?string $relayState = null;
55
56
57
    /**
58
     * Set whether this message was validated externally.
59
     *
60
     * @param bool $messageValidated  TRUE if the message is already validated, FALSE if not.
61
     */
62
    public function setMessageValidated(bool $messageValidated): void
63
    {
64
        $this->messageValidated = $messageValidated;
65
    }
66
67
68
    /**
69
     * @param string $xml
70
     * @throws \Exception
71
     */
72
    public function setXML(string $xml): void
73
    {
74
        try {
75
            $this->dom = DOMDocumentFactory::fromString(str_replace("\r", "", $xml));
76
        } catch (Exception $e) {
77
            throw new Exception('Unable to parse AuthnResponse XML.');
78
        }
79
    }
80
81
82
    /**
83
     * @param string|null $relayState
84
     */
85
    public function setRelayState(?string $relayState): void
86
    {
87
        $this->relayState = $relayState;
88
    }
89
90
91
    /**
92
     * @return string|null
93
     */
94
    public function getRelayState(): ?string
95
    {
96
        return $this->relayState;
97
    }
98
99
100
    /**
101
     * @throws \SimpleSAML\Error\Exception
102
     * @return bool
103
     */
104
    public function validate(): bool
105
    {
106
        Assert::isInstanceOf($this->dom, DOMDocument::class);
107
108
        if ($this->messageValidated) {
109
            // This message was validated externally
110
            return true;
111
        }
112
113
        // Validate the signature
114
        $this->validator = new Validator($this->dom, ['ResponseID', 'AssertionID']);
0 ignored issues
show
Bug introduced by
It seems like $this->dom can also be of type null; however, parameter $xmlNode of SimpleSAML\XML\Validator::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
        $this->validator = new Validator(/** @scrutinizer ignore-type */ $this->dom, ['ResponseID', 'AssertionID']);
Loading history...
115
116
        // Get the issuer of the response
117
        $issuer = $this->getIssuer();
118
119
        // Get the metadata of the issuer
120
        $metadata = MetaDataStorageHandler::getMetadataHandler();
121
        $md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote');
122
123
        $publicKeys = $md->getPublicKeys('signing');
124
        if (!empty($publicKeys)) {
125
            $certFingerprints = [];
126
            foreach ($publicKeys as $key) {
127
                if ($key['type'] !== 'X509Certificate') {
128
                    continue;
129
                }
130
                $certFingerprints[] = sha1(base64_decode($key['X509Certificate']));
131
            }
132
            $this->validator->validateFingerprint($certFingerprints);
0 ignored issues
show
Bug introduced by
The method validateFingerprint() does not exist on SimpleSAML\XML\Validator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
            $this->validator->/** @scrutinizer ignore-call */ 
133
                              validateFingerprint($certFingerprints);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
133
        } elseif ($md->hasValue('certFingerprint')) {
134
            $certFingerprints = $md->getArrayizeString('certFingerprint');
135
136
            // Validate the fingerprint
137
            $this->validator->validateFingerprint($certFingerprints);
138
        } elseif ($md->hasValue('caFile')) {
139
            // Validate against CA
140
            $configUtils = new Utils\Config();
141
            $this->validator->validateCA($configUtils->getCertPath($md->getString('caFile')));
0 ignored issues
show
Bug introduced by
The method validateCA() does not exist on SimpleSAML\XML\Validator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

141
            $this->validator->/** @scrutinizer ignore-call */ 
142
                              validateCA($configUtils->getCertPath($md->getString('caFile')));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
142
        } else {
143
            throw new Error\Exception(
144
                'Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].',
145
            );
146
        }
147
148
        return true;
149
    }
150
151
152
    /**
153
     * Checks if the given node is validated by the signature on this response.
154
     *
155
     * @param \DOMElement|\SimpleXMLElement $node Node to be validated.
156
     * @return bool TRUE if the node is validated or FALSE if not.
157
     */
158
    private function isNodeValidated($node): bool
159
    {
160
        if ($this->messageValidated) {
161
            // This message was validated externally
162
            return true;
163
        }
164
165
        if ($this->validator === null) {
166
            return false;
167
        }
168
169
        // Convert the node to a DOM node if it is an element from SimpleXML
170
        if ($node instanceof SimpleXMLElement) {
171
            $node = dom_import_simplexml($node);
172
        }
173
174
        Assert::isInstanceOf($node, DOMNode::class);
175
176
        return $this->validator->isNodeValidated($node);
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type null; however, parameter $node of SimpleSAML\XML\Validator::isNodeValidated() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        return $this->validator->isNodeValidated(/** @scrutinizer ignore-type */ $node);
Loading history...
177
    }
178
179
180
    /**
181
     * This function runs an xPath query on this authentication response.
182
     *
183
     * @param string $query   The query which should be run.
184
     * @param \DOMNode $node  The node which this query is relative to. If this node is NULL (the default)
185
     *                        then the query will be relative to the root of the response.
186
     * @return \DOMNodeList
187
     */
188
    private function doXPathQuery(string $query, DOMNode $node = null): DOMNodeList
189
    {
190
        Assert::isInstanceOf($node, DOMNode::class);
191
192
        if ($node === null) {
193
            $node = $this->dom->documentElement;
194
        }
195
196
        Assert::isInstanceOf($node, DOMNode::class);
197
198
        $xPath = new DOMXpath($this->dom);
0 ignored issues
show
Bug introduced by
It seems like $this->dom can also be of type null; however, parameter $document of DOMXPath::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

198
        $xPath = new DOMXpath(/** @scrutinizer ignore-type */ $this->dom);
Loading history...
199
        $xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS);
200
        $xPath->registerNamespace('shib', self::SHIB_ASSERT_NS);
201
202
        return $xPath->query($query, $node);
203
    }
204
205
206
    /**
207
     * Retrieve the session index of this response.
208
     *
209
     * @return string|null  The session index of this response.
210
     */
211
    public function getSessionIndex(): ?string
212
    {
213
        $query = '/shibp:Response/shib:Assertion/shib:AuthnStatement';
214
        $nodelist = $this->doXPathQuery($query);
215
216
        if ($node = $nodelist->item(0)) {
217
            return $node->getAttribute('SessionIndex');
218
        }
219
220
        return null;
221
    }
222
223
224
    /**
225
     * @throws \Exception
226
     * @return array
227
     */
228
    public function getAttributes(): array
229
    {
230
        $metadata = MetaDataStorageHandler::getMetadataHandler();
231
        $md = $metadata->getMetaData($this->getIssuer(), 'shib13-idp-remote');
232
        $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;
233
234
        if (!($this->dom instanceof DOMDocument)) {
235
            return [];
236
        }
237
238
        $attributes = [];
239
240
        $assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion');
241
242
        foreach ($assertions as $assertion) {
243
            if (!$this->isNodeValidated($assertion)) {
244
                throw new Exception('Shib13 AuthnResponse contained an unsigned assertion.');
245
            }
246
247
            $conditions = $this->doXPathQuery('shib:Conditions', $assertion);
248
            if ($conditions->length > 0) {
249
                $condition = $conditions->item(0);
250
251
                $start = $condition->getAttribute('NotBefore');
252
                $end = $condition->getAttribute('NotOnOrAfter');
253
254
                if ($start && $end) {
255
                    if (!self::checkDateConditions($start, $end)) {
256
                        error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')');
257
                        continue;
258
                    }
259
                }
260
            }
261
262
            $attribute_nodes = $this->doXPathQuery(
263
                'shib:AttributeStatement/shib:Attribute/shib:AttributeValue',
264
                $assertion,
265
            );
266
267
            foreach ($attribute_nodes as $attribute) {
268
                /** @var \DOMElement $attribute */
269
270
                $value = $attribute->textContent;
271
                /** @var \DOMElement $parentNode */
272
                $parentNode = $attribute->parentNode;
273
                $name = $parentNode->getAttribute('AttributeName');
274
275
                if ($attribute->hasAttribute('Scope')) {
276
                    $scopePart = '@' . $attribute->getAttribute('Scope');
277
                } else {
278
                    $scopePart = '';
279
                }
280
281
                if (empty($name)) {
282
                    throw new Exception('Shib13 Attribute node without an AttributeName.');
283
                }
284
285
                if (!array_key_exists($name, $attributes)) {
286
                    $attributes[$name] = [];
287
                }
288
289
                if ($base64) {
290
                    $encodedvalues = explode('_', $value);
291
                    foreach ($encodedvalues as $v) {
292
                        $attributes[$name][] = base64_decode($v) . $scopePart;
293
                    }
294
                } else {
295
                    $attributes[$name][] = $value . $scopePart;
296
                }
297
            }
298
        }
299
300
        return $attributes;
301
    }
302
303
304
    /**
305
     * @throws \Exception
306
     * @return string
307
     */
308
    public function getIssuer(): string
309
    {
310
        $query = '/shibp:Response/shib:Assertion/@Issuer';
311
        $nodelist = $this->doXPathQuery($query);
312
313
        if ($attr = $nodelist->item(0)) {
314
            return $attr->value;
315
        } else {
316
            throw new Exception('Could not find Issuer field in Authentication response');
317
        }
318
    }
319
320
321
    /**
322
     * @return array
323
     */
324
    public function getNameID(): array
325
    {
326
        $nameID = [];
327
328
        $query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier';
329
        $nodelist = $this->doXPathQuery($query);
330
331
        if ($node = $nodelist->item(0)) {
332
            $nameID["Value"] = $node->nodeValue;
333
            $nameID["Format"] = $node->getAttribute('Format');
334
        }
335
336
        return $nameID;
337
    }
338
339
340
    /**
341
     * Build a authentication response.
342
     *
343
     * @param \SimpleSAML\Configuration $idp Metadata for the IdP the response is sent from.
344
     * @param \SimpleSAML\Configuration $sp Metadata for the SP the response is sent to.
345
     * @param string $shire The endpoint on the SP the response is sent to.
346
     * @param array|null $attributes The attributes which should be included in the response.
347
     * @return string The response.
348
     */
349
    public function generate(Configuration $idp, Configuration $sp, string $shire, ?array $attributes): string
350
    {
351
        if ($sp->hasValue('scopedattributes')) {
352
            $scopedAttributes = $sp->getArray('scopedattributes');
353
        } elseif ($idp->hasValue('scopedattributes')) {
354
            $scopedAttributes = $idp->getArray('scopedattributes');
355
        } else {
356
            $scopedAttributes = [];
357
        }
358
359
        $randomUtils = new Random();
360
        $timeUtils = new Utils\Time();
361
362
        $id = $randomUtils->generateID();
363
        $issueInstant = $timeUtils->generateTimestamp();
364
365
        // 30 seconds timeskew back in time to allow differing clocks
366
        $notBefore = $timeUtils->generateTimestamp(time() - 30);
367
368
        $assertionExpire = $timeUtils->generateTimestamp(time() + 300); // 5 minutes
369
        $assertionid = $randomUtils->generateID();
370
371
        $spEntityId = $sp->getString('entityid');
372
373
        $audience = $sp->getOptionalString('audience', $spEntityId);
374
        $base64 = $sp->getOptionalBoolean('base64attributes', false);
375
376
        $namequalifier = $sp->getOptionalString('NameQualifier', $spEntityId);
377
        $nameid = $randomUtils->generateID();
378
        $subjectNode =
379
            '<Subject>' .
380
            '<NameIdentifier' .
381
            ' Format="urn:mace:shibboleth:1.0:nameIdentifier"' .
382
            ' NameQualifier="' . htmlspecialchars($namequalifier) . '"' .
0 ignored issues
show
Bug introduced by
It seems like $namequalifier can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

382
            ' NameQualifier="' . htmlspecialchars(/** @scrutinizer ignore-type */ $namequalifier) . '"' .
Loading history...
383
            '>' .
384
            htmlspecialchars($nameid) .
385
            '</NameIdentifier>' .
386
            '<SubjectConfirmation>' .
387
            '<ConfirmationMethod>' .
388
            'urn:oasis:names:tc:SAML:1.0:cm:bearer' .
389
            '</ConfirmationMethod>' .
390
            '</SubjectConfirmation>' .
391
            '</Subject>';
392
393
        $encodedattributes = '';
394
395
        if (is_array($attributes)) {
0 ignored issues
show
introduced by
The condition is_array($attributes) is always true.
Loading history...
396
            $encodedattributes .= '<AttributeStatement>';
397
            $encodedattributes .= $subjectNode;
398
399
            foreach ($attributes as $name => $value) {
400
                $encodedattributes .= $this->encAttribute($name, $value, $base64, $scopedAttributes);
0 ignored issues
show
Bug introduced by
It seems like $base64 can also be of type null; however, parameter $base64 of SimpleSAML\Module\casser...esponse::encAttribute() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

400
                $encodedattributes .= $this->encAttribute($name, $value, /** @scrutinizer ignore-type */ $base64, $scopedAttributes);
Loading history...
401
            }
402
403
            $encodedattributes .= '</AttributeStatement>';
404
        }
405
406
        /*
407
         * The SAML 1.1 response message
408
         */
409
        $response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
410
    xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
411
    xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
412
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="' . $issueInstant . '"
413
    MajorVersion="1" MinorVersion="1"
414
    Recipient="' . htmlspecialchars($shire) . '" ResponseID="' . $id . '">
415
    <Status>
416
        <StatusCode Value="samlp:Success" />
417
    </Status>
418
    <Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion"
419
        AssertionID="' . $assertionid . '" IssueInstant="' . $issueInstant . '"
420
        Issuer="' . htmlspecialchars($idp->getString('entityid')) . '" MajorVersion="1" MinorVersion="1">
421
        <Conditions NotBefore="' . $notBefore . '" NotOnOrAfter="' . $assertionExpire . '">
422
            <AudienceRestrictionCondition>
423
                <Audience>' . htmlspecialchars($audience) . '</Audience>
424
            </AudienceRestrictionCondition>
425
        </Conditions>
426
        <AuthenticationStatement AuthenticationInstant="' . $issueInstant . '"
427
            AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:unspecified">' .
428
            $subjectNode . '
429
        </AuthenticationStatement>
430
        ' . $encodedattributes . '
431
    </Assertion>
432
</Response>';
433
434
        return $response;
435
    }
436
437
438
    /**
439
     * Format a shib13 attribute.
440
     *
441
     * @param string $name  Name of the attribute.
442
     * @param array $values  Values of the attribute (as an array of strings).
443
     * @param bool $base64  Whether the attriubte values should be base64-encoded.
444
     * @param array $scopedAttributes  Array of attributes names which are scoped.
445
     * @return string  The attribute encoded as an XML-string.
446
     */
447
    private function encAttribute(string $name, array $values, bool $base64, array $scopedAttributes): string
448
    {
449
        if (in_array($name, $scopedAttributes, true)) {
450
            $scoped = true;
451
        } else {
452
            $scoped = false;
453
        }
454
455
        $attr = '<Attribute AttributeName="' . htmlspecialchars($name) .
456
            '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">';
457
        foreach ($values as $value) {
458
            $scopePart = '';
459
            if ($scoped) {
460
                $tmp = explode('@', $value, 2);
461
                if (count($tmp) === 2) {
462
                    $value = $tmp[0];
463
                    $scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"';
464
                }
465
            }
466
467
            if ($base64) {
468
                $value = base64_encode($value);
469
            }
470
471
            $attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>';
472
        }
473
        $attr .= '</Attribute>';
474
475
        return $attr;
476
    }
477
478
    /**
479
     * Check if we are currently between the given date & time conditions.
480
     *
481
     * Note that this function allows a 10-minute leap from the initial time as marked by $start.
482
     *
483
     * @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which
484
     *     case there's no limitations in the past.
485
     * @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which
486
     *     case there's no limitations in the future.
487
     *
488
     * @return bool True if the current time belongs to the period specified by $start and $end. False otherwise.
489
     *
490
     * @see \SAML2\Utils::xsDateTimeToTimestamp.
491
     *
492
     */
493
    protected static function checkDateConditions(string $start = null, string $end = null): bool
494
    {
495
        $currentTime = time();
496
497
        if (!empty($start)) {
498
            $startTime = XMLUtils::xsDateTimeToTimestamp($start);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\XML\Utils::xsDateTimeToTimestamp() has been deprecated: Use DateTime objects instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

498
            $startTime = /** @scrutinizer ignore-deprecated */ XMLUtils::xsDateTimeToTimestamp($start);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
499
            // allow for a 10 minute difference in time
500
            if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
501
                return false;
502
            }
503
        }
504
        if (!empty($end)) {
505
            $endTime = XMLUtils::xsDateTimeToTimestamp($end);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\XML\Utils::xsDateTimeToTimestamp() has been deprecated: Use DateTime objects instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

505
            $endTime = /** @scrutinizer ignore-deprecated */ XMLUtils::xsDateTimeToTimestamp($end);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
506
            if (($endTime < 0) || ($endTime <= $currentTime)) {
507
                return false;
508
            }
509
        }
510
        return true;
511
    }
512
}
513