MetadataBuilder   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 142
c 1
b 0
f 0
dl 0
loc 305
rs 8.96
wmc 43

10 Methods

Rating   Name   Duplication   Size   Complexity  
A buildKeyDescriptor() 0 16 2
A getContactPerson() 0 11 4
A getRoleDescriptor() 0 14 2
A signDocument() 0 23 2
F getExtensions() 0 73 16
A getSecurityTokenService() 0 16 1
A __construct() 0 5 1
A buildDocument() 0 21 2
B getKeyDescriptor() 0 30 8
A getOrganization() 0 28 5

How to fix   Complexity   

Complex Class

Complex classes like MetadataBuilder 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 MetadataBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\adfs\IdP;
6
7
use Beste\Clock\LocalizedClock;
8
use Exception;
9
use Psr\Clock\ClockInterface;
10
use SimpleSAML\{Configuration, Logger, Module, Utils};
11
use SimpleSAML\Assert\Assert;
12
use SimpleSAML\SAML2\Exception\ArrayValidationException;
13
use SimpleSAML\SAML2\XML\md\AbstractMetadataDocument;
14
use SimpleSAML\SAML2\XML\md\ContactPerson;
15
use SimpleSAML\SAML2\XML\md\EntityDescriptor;
16
use SimpleSAML\SAML2\XML\md\Extensions;
17
use SimpleSAML\SAML2\XML\md\KeyDescriptor;
18
use SimpleSAML\SAML2\XML\md\Organization;
19
use SimpleSAML\SAML2\XML\mdattr\EntityAttributes;
20
use SimpleSAML\SAML2\XML\mdrpi\RegistrationInfo;
21
use SimpleSAML\SAML2\XML\mdui\{DiscoHints, UIInfo};
22
use SimpleSAML\SAML2\XML\saml\{Attribute, AttributeValue};
23
use SimpleSAML\SAML2\XML\shibmd\Scope;
24
use SimpleSAML\XML\Chunk;
25
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
26
use SimpleSAML\XMLSecurity\Key\PrivateKey;
27
use SimpleSAML\XMLSecurity\XML\ds\{KeyInfo, KeyName, X509Certificate, X509Data};
28
use SimpleSAML\WSSecurity\Constants as C;
29
use SimpleSAML\WSSecurity\XML\fed\{
30
    PassiveRequestorEndpoint,
31
    SecurityTokenServiceEndpoint,
32
    SecurityTokenServiceType,
33
    TokenTypesOffered,
34
    TokenType,
35
};
36
use SimpleSAML\WSSecurity\XML\wsa_200508\{Address, EndpointReference};
37
38
use function array_key_exists;
39
use function preg_match;
40
41
/**
42
 * Common code for building SAML 2 metadata based on the available configuration.
43
 *
44
 * @package simplesamlphp/simplesamlphp-module-adfs
45
 */
46
class MetadataBuilder
47
{
48
    /** @var \Psr\Clock\ClockInterface */
49
    protected ClockInterface $clock;
50
51
    /**
52
     * Constructor.
53
     *
54
     * @param \SimpleSAML\Configuration $config The general configuration
55
     * @param \SimpleSAML\Configuration $metadata The metadata configuration
56
     */
57
    public function __construct(
58
        protected Configuration $config,
59
        protected Configuration $metadata,
60
    ) {
61
        $this->clock = LocalizedClock::in('Z');
62
    }
63
64
65
    /**
66
     * Build a metadata document
67
     *
68
     * @return \SimpleSAML\SAML2\XML\md\EntityDescriptor
69
     */
70
    public function buildDocument(): EntityDescriptor
71
    {
72
        $entityId = $this->metadata->getString('entityid');
73
        $contactPerson = $this->getContactPerson();
74
        $organization = $this->getOrganization();
75
        $roleDescriptor = $this->getRoleDescriptor();
76
77
        $randomUtils = new Utils\Random();
78
        $entityDescriptor = new EntityDescriptor(
79
            id: $randomUtils->generateID(),
80
            entityId: $entityId,
81
            contactPerson: $contactPerson,
82
            organization: $organization,
83
            roleDescriptor: $roleDescriptor,
84
        );
85
86
        if ($this->config->getOptionalBoolean('metadata.sign.enable', false) === true) {
87
            $this->signDocument($entityDescriptor);
88
        }
89
90
        return $entityDescriptor;
91
    }
92
93
94
    /**
95
     * @param \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument $document
96
     * @return \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument
97
     */
98
    protected function signDocument(AbstractMetadataDocument $document): AbstractMetadataDocument
99
    {
100
        $cryptoUtils = new Utils\Crypto();
101
102
        /** @var array $keyArray */
103
        $keyArray = $cryptoUtils->loadPrivateKey($this->config, true, 'metadata.sign.');
104
        $certArray = $cryptoUtils->loadPublicKey($this->config, false, 'metadata.sign.');
105
        $algo = $this->config->getOptionalString('metadata.sign.algorithm', C::SIG_RSA_SHA256);
106
107
        $key = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password'] ?? '');
108
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm($algo, $key);
0 ignored issues
show
Bug introduced by
It seems like $algo can also be of type null; however, parameter $algId of SimpleSAML\XMLSecurity\A...Factory::getAlgorithm() 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

108
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(/** @scrutinizer ignore-type */ $algo, $key);
Loading history...
109
110
        $keyInfo = null;
111
        if ($certArray !== null) {
112
            $keyInfo = new KeyInfo([
113
                new X509Data([
114
                    new X509Certificate($certArray['certData']),
115
                ]),
116
            ]);
117
        }
118
119
        $document->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
120
        return $document;
121
    }
122
123
124
    /**
125
     * This method builds the md:Organization element, if any
126
     */
127
    private function getOrganization(): ?Organization
128
    {
129
        if (
130
            !$this->metadata->hasValue('OrganizationName') ||
131
            !$this->metadata->hasValue('OrganizationDisplayName') ||
132
            !$this->metadata->hasValue('OrganizationURL')
133
        ) {
134
            // empty or incomplete organization information
135
            return null;
136
        }
137
138
        $arrayUtils = new Utils\Arrays();
139
        $org = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $org is dead and can be removed.
Loading history...
140
141
        try {
142
            $org = Organization::fromArray([
143
                'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'),
144
                'OrganizationDisplayName' => $arrayUtils->arrayize(
145
                    $this->metadata->getArray('OrganizationDisplayName'),
146
                    'en',
147
                ),
148
                'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'),
149
            ]);
150
        } catch (ArrayValidationException $e) {
151
            Logger::error('Federation: invalid content found in contact: ' . $e->getMessage());
152
        }
153
154
        return $org;
155
    }
156
157
158
    /**
159
     * This method builds the role descriptor elements
160
     */
161
    private function getRoleDescriptor(): array
162
    {
163
        $descriptors = [];
164
165
        $set = $this->metadata->getString('metadata-set');
166
        switch ($set) {
167
            case 'adfs-idp-hosted':
168
                $descriptors[] = $this->getSecurityTokenService();
169
                break;
170
            default:
171
                throw new Exception('Not implemented');
172
        }
173
174
        return $descriptors;
175
    }
176
177
178
    /**
179
     * This method builds the SecurityTokenService element
180
     */
181
    public function getSecurityTokenService(): SecurityTokenServiceType
182
    {
183
        $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php';
184
185
        return new SecurityTokenServiceType(
186
            protocolSupportEnumeration: [C::NS_TRUST_200512, C::NS_TRUST_200502, C::NS_FED],
187
            keyDescriptors: $this->getKeyDescriptor(),
188
            tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]),
189
            securityTokenServiceEndpoint: [
190
                new SecurityTokenServiceEndpoint([
191
                    new EndpointReference(new Address($defaultEndpoint)),
192
                ]),
193
            ],
194
            passiveRequestorEndpoint: [
195
                new PassiveRequestorEndpoint([
196
                    new EndpointReference(new Address($defaultEndpoint)),
197
                ]),
198
            ],
199
        );
200
    }
201
202
203
    /**
204
     * This method builds the md:KeyDescriptor elements, if any
205
     */
206
    private function getKeyDescriptor(): array
207
    {
208
        $keyDescriptor = [];
209
210
        $keys = $this->metadata->getPublicKeys();
211
        foreach ($keys as $key) {
212
            if ($key['type'] !== 'X509Certificate') {
213
                continue;
214
            }
215
            if (!isset($key['signing']) || $key['signing'] === true) {
216
                $keyDescriptor[] = self::buildKeyDescriptor(
217
                    'signing',
218
                    $key['X509Certificate'],
219
                    $key['name'] ?? null,
220
                );
221
            }
222
            if (!isset($key['encryption']) || $key['encryption'] === true) {
223
                $keyDescriptor[] = self::buildKeyDescriptor(
224
                    'encryption',
225
                    $key['X509Certificate'],
226
                    $key['name'] ?? null,
227
                );
228
            }
229
        }
230
231
        if ($this->metadata->hasValue('https.certData')) {
232
            $keyDescriptor[] = self::buildKeyDescriptor('signing', $this->metadata->getString('https.certData'), null);
233
        }
234
235
        return $keyDescriptor;
236
    }
237
238
239
    /**
240
     * This method builds the md:ContactPerson elements, if any
241
     */
242
    private function getContactPerson(): array
243
    {
244
        $contacts = [];
245
246
        foreach ($this->metadata->getOptionalArray('contacts', []) as $contact) {
247
            if (array_key_exists('ContactType', $contact) && array_key_exists('EmailAddress', $contact)) {
248
                $contacts[] = ContactPerson::fromArray($contact);
249
            }
250
        }
251
252
        return $contacts;
253
    }
254
255
256
    /**
257
     * This method builds the md:Extensions, if any
258
     */
259
    private function getExtensions(): ?Extensions
0 ignored issues
show
Unused Code introduced by
The method getExtensions() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
260
    {
261
        $extensions = [];
262
263
        if ($this->metadata->hasValue('scope')) {
264
            foreach ($this->metadata->getArray('scope') as $scopetext) {
265
                $isRegexpScope = (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext));
266
                $extensions[] = new Scope($scopetext, $isRegexpScope);
267
            }
268
        }
269
270
        if ($this->metadata->hasValue('EntityAttributes')) {
271
            $attr = [];
272
            foreach ($this->metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
273
                $attrValues = [];
274
                foreach ($attributeValues as $attributeValue) {
275
                    $attrValues[] = new AttributeValue($attributeValue);
276
                }
277
278
                // Attribute names that is not URI is prefixed as this: '{nameformat}name'
279
                if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
280
                    $attr[] = new Attribute(
281
                        name: $matches[2],
282
                        nameFormat: $matches[1] === C::NAMEFORMAT_UNSPECIFIED ? null : $matches[1],
283
                        attributeValue: $attrValues,
284
                    );
285
                } else {
286
                    $attr[] = new Attribute(
287
                        name: $attributeName,
288
                        nameFormat: C::NAMEFORMAT_UNSPECIFIED,
289
                        attributeValue: $attrValues,
290
                    );
291
                }
292
            }
293
294
            $extensions[] = new EntityAttributes($attr);
295
        }
296
297
        if ($this->metadata->hasValue('saml:Extensions')) {
298
            $chunks = $this->metadata->getArray('saml:Extensions');
299
            Assert::allIsInstanceOf($chunks, Chunk::class);
300
            $extensions = array_merge($extensions, $chunks);
301
        }
302
303
        if ($this->metadata->hasValue('RegistrationInfo')) {
304
            try {
305
                $extensions[] = RegistrationInfo::fromArray($this->metadata->getArray('RegistrationInfo'));
306
            } catch (ArrayValidationException $err) {
307
                Logger::error('Metadata: invalid content found in RegistrationInfo: ' . $err->getMessage());
308
            }
309
        }
310
311
        if ($this->metadata->hasValue('UIInfo')) {
312
            try {
313
                $extensions[] = UIInfo::fromArray($this->metadata->getArray('UIInfo'));
314
            } catch (ArrayValidationException $err) {
315
                Logger::error('Metadata: invalid content found in UIInfo: ' . $err->getMessage());
316
            }
317
        }
318
319
        if ($this->metadata->hasValue('DiscoHints')) {
320
            try {
321
                $extensions[] = DiscoHints::fromArray($this->metadata->getArray('DiscoHints'));
322
            } catch (ArrayValidationException $err) {
323
                Logger::error('Metadata: invalid content found in DiscoHints: ' . $err->getMessage());
324
            }
325
        }
326
327
        if ($extensions !== []) {
328
            return new Extensions($extensions);
329
        }
330
331
        return null;
332
    }
333
334
335
    private static function buildKeyDescriptor(string $use, string $x509Cert, ?string $keyName): KeyDescriptor
336
    {
337
        Assert::oneOf($use, ['encryption', 'signing']);
338
        $info = [
339
            new X509Data([
340
                new X509Certificate($x509Cert),
341
            ]),
342
        ];
343
344
        if ($keyName !== null) {
345
            $info[] = new KeyName($keyName);
346
        }
347
348
        return new KeyDescriptor(
349
            new KeyInfo($info),
350
            $use,
351
        );
352
    }
353
}
354