Metadata   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 21
dl 0
loc 334
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedBindings() 0 4 1
A supportsRedirect() 0 4 1
A supportsPost() 0 4 1
A fetchByUrl() 0 10 1
A create() 0 42 4
A createDescriptor() 0 20 3
A createIdpDescriptor() 0 44 2
A createSpDescriptor() 0 51 3
A addSloEndpoint() 0 32 1
A setCertificate() 0 35 2
A updateDescriptorCertificates() 0 4 1
A setSign() 0 4 1
A setEncrypt() 0 4 1
1
<?php
2
3
namespace flipbox\saml\core\services;
4
5
use craft\base\Component;
6
use flipbox\keychain\records\KeyChainRecord;
7
use flipbox\saml\core\helpers\SecurityHelper;
8
use flipbox\saml\core\helpers\UrlHelper;
9
use flipbox\saml\core\models\AbstractSettings;
10
use flipbox\saml\core\models\SettingsInterface;
11
use flipbox\saml\core\records\AbstractProvider;
12
use flipbox\saml\core\records\traits\KeyChain;
13
use GuzzleHttp\Client;
14
use SAML2\Certificate\Key;
15
use SAML2\Constants;
16
use SAML2\DOMDocumentFactory;
17
use SAML2\XML\ds\KeyInfo;
18
use SAML2\XML\ds\X509Certificate;
19
use SAML2\XML\ds\X509Data;
20
use SAML2\XML\md\EndpointType;
21
use SAML2\XML\md\EntityDescriptor;
22
use SAML2\XML\md\IDPSSODescriptor;
23
use SAML2\XML\md\IndexedEndpointType;
24
use SAML2\XML\md\KeyDescriptor;
25
use SAML2\XML\md\SPSSODescriptor;
26
use SAML2\XML\md\SSODescriptorType;
27
use yii\base\Event;
28
use yii\base\InvalidConfigException;
29
30
/**
31
 * Class AbstractMetadata
32
 * @package flipbox\saml\core\services\messages
33
 */
34
class Metadata extends Component
35
{
36
37
    const SET_SIGNING = Key::USAGE_SIGNING;
38
    const SET_ENCRYPTION = Key::USAGE_ENCRYPTION;
39
    const PROTOCOL = Constants::NS_SAMLP;
40
41
    const EVENT_AFTER_MESSAGE_CREATED = 'eventAfterMessageCreated';
42
43
    /**
44
     * @var array
45
     */
46
    protected $supportedBindings = [
47
        Constants::BINDING_HTTP_POST,
48
    ];
49
50
    /**
51
     * @return array
52
     */
53
    public function getSupportedBindings()
54
    {
55
        return $this->supportedBindings;
56
    }
57
58
    /**
59
     * @return bool
60
     */
61
    protected function supportsRedirect()
62
    {
63
        return in_array(Constants::BINDING_HTTP_REDIRECT, $this->getSupportedBindings());
64
    }
65
66
    /**
67
     * @return bool
68
     */
69
    protected function supportsPost()
70
    {
71
        return in_array(Constants::BINDING_HTTP_POST, $this->getSupportedBindings());
72
    }
73
74
    /**
75
     * @param string $url
76
     * @return EntityDescriptor
77
     * @throws \Exception
78
     */
79
    public function fetchByUrl(string $url)
80
    {
81
        $client = new Client();
82
        $response = $client->get($url);
83
        return new EntityDescriptor(
84
            DOMDocumentFactory::fromString(
85
                $response->getBody()->getContents()
86
            )->documentElement
87
        );
88
    }
89
90
    /**
91
     * @param SettingsInterface $settings
92
     * @param KeyChainRecord|null $withKeyPair
0 ignored issues
show
Bug introduced by
There is no parameter named $withKeyPair. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
93
     * @return EntityDescriptor
94
     * @throws InvalidConfigException
95
     */
96
    public function create(
97
        SettingsInterface $settings,
98
        AbstractProvider $provider
99
    ): EntityDescriptor {
100
101
        $entityDescriptor = new EntityDescriptor();
102
103
        $entityId = $provider ? $provider->entityId : $settings->getEntityId();
0 ignored issues
show
Documentation introduced by
The property entityId does not exist on object<flipbox\saml\core...cords\AbstractProvider>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
104
105
        $entityDescriptor->setEntityID($entityId);
106
107
        foreach ($this->getSupportedBindings() as $binding) {
108
            $entityDescriptor->addRoleDescriptor(
109
                $descriptor = $this->createDescriptor(
110
                    $binding,
111
                    $settings,
112
                    $provider
113
                )
114
            );
115
116
            /**
117
             * Add security settings
118
             */
119
            if ($provider->keychain) {
120
                $this->setEncrypt($descriptor, $provider->keychain);
121
                $this->setSign($descriptor, $provider->keychain);
122
            }
123
        }
124
125
        /**
126
         * Kick off event here so people can manipulate this object if needed
127
         */
128
        $event = new Event();
129
130
        /**
131
         * response
132
         */
133
        $event->data = $entityDescriptor;
134
        $this->trigger(static::EVENT_AFTER_MESSAGE_CREATED, $event);
135
136
        return $entityDescriptor;
137
    }
138
139
    /**
140
     * @param string $binding
141
     * @return IdpSsoDescriptor|SpSsoDescriptor
142
     * @throws InvalidConfigException
143
     */
144
    protected function createDescriptor(
145
        string $binding,
146
        SettingsInterface $settings,
147
        AbstractProvider $provider
148
    ) {
149
        if (! in_array($binding, [
150
            Constants::BINDING_HTTP_POST,
151
            Constants::BINDING_HTTP_REDIRECT,
152
        ])) {
153
            throw new InvalidConfigException('Binding not supported: ' . $binding);
154
        }
155
156
        if ($settings->getMyType() === $settings::SP) {
157
            $descriptor = $this->createSpDescriptor($binding, $settings, $provider);
0 ignored issues
show
Compatibility introduced by
$settings of type object<flipbox\saml\core...dels\SettingsInterface> is not a sub-type of object<flipbox\saml\core\models\AbstractSettings>. It seems like you assume a concrete implementation of the interface flipbox\saml\core\models\SettingsInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
158
        } else {
159
            $descriptor = $this->createIdpDescriptor($binding, $settings, $provider);
0 ignored issues
show
Compatibility introduced by
$settings of type object<flipbox\saml\core...dels\SettingsInterface> is not a sub-type of object<flipbox\saml\core\models\AbstractSettings>. It seems like you assume a concrete implementation of the interface flipbox\saml\core\models\SettingsInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
160
        }
161
162
        return $descriptor;
163
    }
164
165
    /**
166
     * @param string $binding
167
     * @return IDPSSODescriptor
168
     */
169
    protected function createIdpDescriptor(
170
        string $binding,
171
        AbstractSettings $settings,
172
        AbstractProvider $provider = null
173
    ) {
174
        $descriptor = new \SAML2\XML\md\IDPSSODescriptor();
175
        $descriptor->setProtocolSupportEnumeration([
176
            static::PROTOCOL,
177
        ]);
178
179
        if (property_exists($settings, 'wantsAuthnRequestsSigned')) {
180
            $descriptor->setWantAuthnRequestsSigned(
181
                $settings->wantsAuthnRequestsSigned
0 ignored issues
show
Documentation introduced by
The property wantsAuthnRequestsSigned does not exist on object<flipbox\saml\core\models\AbstractSettings>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
182
            );
183
        }
184
185
        // SSO
186
        $ssoEndpoint = new EndpointType();
187
        $ssoEndpoint->setBinding($binding);
188
        $ssoEndpoint->setLocation(
189
            UrlHelper::buildEndpointUrl(
190
                $settings,
191
                UrlHelper::LOGIN_ENDPOINT,
192
                $provider
0 ignored issues
show
Bug introduced by
It seems like $provider defined by parameter $provider on line 172 can be null; however, flipbox\saml\core\helper...per::buildEndpointUrl() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
193
            )
194
        );
195
196
        $descriptor->setSingleSignOnService([
197
            $ssoEndpoint,
198
        ]);
199
200
        // SLO
201
        $this->addSloEndpoint(
202
            $descriptor,
203
            $settings,
204
            $provider
0 ignored issues
show
Bug introduced by
It seems like $provider defined by parameter $provider on line 172 can be null; however, flipbox\saml\core\servic...adata::addSloEndpoint() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
205
        );
206
207
        // todo add attributes from mapping
208
209
210
211
        return $descriptor;
212
    }
213
214
    /**
215
     * @param string $binding
216
     * @return SPSSODescriptor
217
     */
218
    protected function createSpDescriptor(string $binding, AbstractSettings $settings, AbstractProvider $provider)
219
    {
220
221
        $descriptor = new SPSSODescriptor();
222
        $descriptor->setProtocolSupportEnumeration([
223
            static::PROTOCOL,
224
        ]);
225
226
        if (property_exists($settings, 'wantsSignedAssertions') &&
227
            is_bool($settings->wantsSignedAssertions)
0 ignored issues
show
Documentation introduced by
The property wantsSignedAssertions does not exist on object<flipbox\saml\core\models\AbstractSettings>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
228
        ) {
229
            $descriptor->setWantAssertionsSigned(
230
                $settings->wantsSignedAssertions
0 ignored issues
show
Documentation introduced by
The property wantsSignedAssertions does not exist on object<flipbox\saml\core\models\AbstractSettings>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
231
            );
232
        }
233
234
235
        // ACS
236
        $acsEndpoint = new IndexedEndpointType();
237
        $acsEndpoint->setIndex(1);
238
        $acsEndpoint->setBinding($binding);
239
        $acsEndpoint->setLocation(
240
            UrlHelper::buildEndpointUrl(
241
                $settings,
242
                UrlHelper::LOGIN_ENDPOINT,
243
                $provider
244
            )
245
        );
246
247
        $descriptor->setAssertionConsumerService([
248
            $acsEndpoint,
249
        ]);
250
251
        //SLO
252
        $this->addSloEndpoint(
253
            $descriptor,
254
            $settings,
255
            $provider
256
        );
257
258
        //todo add attribute consuming service
259
//        var_dump(
260
//            $provider->getMapping()
261
//        );exit;
262
//        $attributeConsumingService = new AttributeConsumingService();
263
//        $attributeConsumingService->addRequestedAttribute($att = new RequestedAttribute());
264
//        $att->setName('username');
265
//        $descriptor->addAttributeConsumingService($attributeConsumingService);
266
267
        return $descriptor;
268
    }
269
270
    protected function addSloEndpoint(
271
        SSODescriptorType $descriptorType,
272
        AbstractSettings $settings,
273
        AbstractProvider $provider
274
    ) {
275
        $sloEndpointRedirect = new EndpointType();
276
        $sloEndpointRedirect->setBinding(
277
            Constants::BINDING_HTTP_REDIRECT
278
        );
279
280
        $sloLogoutEndpointUrl = UrlHelper::buildEndpointUrl(
281
            $settings,
282
            UrlHelper::LOGOUT_ENDPOINT,
283
            $provider
284
        );
285
        $sloEndpointRedirect->setLocation(
286
            $sloLogoutEndpointUrl
287
        );
288
289
        $sloEndpointPost = new EndpointType();
290
        $sloEndpointPost->setBinding(
291
            Constants::BINDING_HTTP_POST
292
        );
293
        $sloEndpointPost->setLocation(
294
            $sloLogoutEndpointUrl
295
        );
296
297
        $descriptorType->setSingleLogoutService([
298
            $sloEndpointRedirect,
299
            $sloEndpointPost,
300
        ]);
301
    }
302
303
304
305
    /**
306
     * @param SSODescriptorType $ssoDescriptor
307
     * @param KeyChainRecord $keyChainRecord
308
     */
309
    protected function setCertificate(
310
        SSODescriptorType $ssoDescriptor,
311
        KeyChainRecord $keyChainRecord,
312
        string $signOrEncrypt
313
    ) {
314
        /**
315
         * Validate use string
316
         */
317
        if (! in_array($signOrEncrypt, [
318
            self::SET_SIGNING,
319
            self::SET_ENCRYPTION,
320
        ])) {
321
            throw new \InvalidArgumentException('Sign or Encrypt argument can only be "signing" or "encrypt"');
322
        }
323
324
        /**
325
         * Create sub object
326
         */
327
        $keyDescriptor = new KeyDescriptor();
328
        $keyInfo = new KeyInfo();
329
        $x509Data = new X509Data();
330
        $x509Certificate = new X509Certificate();
331
332
        $keyInfo->addInfo($x509Data);
333
334
        $x509Certificate->setCertificate(
335
            SecurityHelper::cleanCertificate($keyChainRecord->getDecryptedCertificate())
0 ignored issues
show
Bug introduced by
It seems like $keyChainRecord->getDecryptedCertificate() targeting flipbox\keychain\records...tDecryptedCertificate() can also be of type boolean; however, flipbox\saml\core\helper...per::cleanCertificate() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like \flipbox\saml\core\helpe...DecryptedCertificate()) targeting flipbox\saml\core\helper...per::cleanCertificate() can also be of type array<integer,string> or null; however, SAML2\XML\ds\X509Certificate::setCertificate() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
336
        );
337
338
        $x509Data->addData($x509Certificate);
339
        $keyDescriptor->setKeyInfo($keyInfo);
340
341
        $keyDescriptor->setUse($signOrEncrypt);
342
        $ssoDescriptor->addKeyDescriptor($keyDescriptor);
343
    }
344
345
    public function updateDescriptorCertificates(SSODescriptorType $ssoDescriptor, KeyChainRecord $keyChainRecord) {
346
        $this->setSign($ssoDescriptor,$keyChainRecord);
347
        $this->setEncrypt($ssoDescriptor,$keyChainRecord);
348
    }
349
350
    /**
351
     * @param SSODescriptorType
352
     * @param KeyChainRecord $keyChainRecord
353
     */
354
    protected function setSign(SSODescriptorType $ssoDescriptor, KeyChainRecord $keyChainRecord)
355
    {
356
        $this->setCertificate($ssoDescriptor, $keyChainRecord, static::SET_SIGNING);
357
    }
358
359
    /**
360
     * @param SSODescriptorType
361
     * @param KeyChainRecord $keyChainRecord
362
     */
363
    protected function setEncrypt(SSODescriptorType $ssoDescriptor, KeyChainRecord $keyChainRecord)
364
    {
365
        $this->setCertificate($ssoDescriptor, $keyChainRecord, static::SET_ENCRYPTION);
366
    }
367
}
368