Completed
Push — master ( ff11b3...fd6c95 )
by Damien
10:30
created

Metadata::fetchByUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 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\models\SettingsInterface;
9
use GuzzleHttp\Client;
10
use Psr\Http\Message\UriInterface;
11
use SAML2\Certificate\Key;
12
use SAML2\Constants;
13
use SAML2\DOMDocumentFactory;
14
use SAML2\XML\ds\KeyInfo;
15
use SAML2\XML\ds\X509Certificate;
16
use SAML2\XML\ds\X509Data;
17
use SAML2\XML\md\EndpointType;
18
use SAML2\XML\md\IndexedEndpointType;
19
use SAML2\XML\md\EntityDescriptor;
20
use SAML2\XML\md\IDPSSODescriptor;
21
use SAML2\XML\md\KeyDescriptor;
22
use SAML2\XML\md\SPSSODescriptor;
23
use SAML2\XML\md\SSODescriptorType;
24
use yii\base\Event;
25
use yii\base\InvalidConfigException;
26
27
/**
28
 * Class AbstractMetadata
29
 * @package flipbox\saml\core\services\messages
30
 */
31
class Metadata extends Component
32
{
33
34
    const SET_SIGNING = Key::USAGE_SIGNING;
35
    const SET_ENCRYPTION = Key::USAGE_ENCRYPTION;
36
    const PROTOCOL = Constants::NS_SAMLP;
37
38
    const EVENT_AFTER_MESSAGE_CREATED = 'eventAfterMessageCreated';
39
40
    /**
41
     * @var array
42
     */
43
    protected $supportedBindings = [
44
        Constants::BINDING_HTTP_POST,
45
    ];
46
47
    /**
48
     * @return array
49
     */
50
    public function getSupportedBindings()
51
    {
52
        return $this->supportedBindings;
53
    }
54
55
    /**
56
     * @return bool
57
     */
58
    protected function supportsRedirect()
59
    {
60
        return in_array(Constants::BINDING_HTTP_REDIRECT, $this->getSupportedBindings());
61
    }
62
63
    /**
64
     * @return bool
65
     */
66
    protected function supportsPost()
67
    {
68
        return in_array(Constants::BINDING_HTTP_POST, $this->getSupportedBindings());
69
    }
70
71
    /**
72
     * @param string $url
73
     * @return EntityDescriptor
74
     * @throws \Exception
75
     */
76
    public function fetchByUrl(string $url)
77
    {
78
        $client = new Client();
79
        $response = $client->get($url);
80
        return new EntityDescriptor(
81
            DOMDocumentFactory::fromString(
82
                $response->getBody()->getContents()
83
            )->documentElement
84
        );
85
    }
86
87
    /**
88
     * @param SettingsInterface $settings
89
     * @param KeyChainRecord|null $withKeyPair
90
     * @return EntityDescriptor
91
     * @throws InvalidConfigException
92
     */
93
    public function create(
94
        SettingsInterface $settings,
95
        KeyChainRecord $withKeyPair = null
96
    ): EntityDescriptor {
97
98
        $entityDescriptor = new EntityDescriptor();
99
100
        $entityId = $settings->getEntityId();
101
102
        $entityDescriptor->setEntityID($entityId);
103
104
        foreach ($this->getSupportedBindings() as $binding) {
105
            $entityDescriptor->addRoleDescriptor(
106
                $descriptor = $this->createDescriptor($binding, $settings)
107
            );
108
109
            /**
110
             * Add security settings
111
             */
112
            if ($withKeyPair) {
113
                $this->setEncrypt($descriptor, $withKeyPair);
114
                $this->setSign($descriptor, $withKeyPair);
115
            }
116
        }
117
118
        /**
119
         * Kick off event here so people can manipulate this object if needed
120
         */
121
        $event = new Event();
122
123
        /**
124
         * response
125
         */
126
        $event->data = $entityDescriptor;
127
        $this->trigger(static::EVENT_AFTER_MESSAGE_CREATED, $event);
128
129
        return $entityDescriptor;
130
    }
131
132
    /**
133
     * @param string $binding
134
     * @return IdpSsoDescriptor|SpSsoDescriptor
135
     * @throws InvalidConfigException
136
     */
137
    protected function createDescriptor(string $binding, SettingsInterface $settings)
138
    {
139
        if (! in_array($binding, [
140
            Constants::BINDING_HTTP_POST,
141
            Constants::BINDING_HTTP_REDIRECT,
142
        ])) {
143
            throw new InvalidConfigException('Binding not supported: ' . $binding);
144
        }
145
146
        if ($settings->getMyType() === $settings::SP) {
147
            $descriptor = $this->createSpDescriptor($binding, $settings);
148
        } else {
149
            $descriptor = $this->createIdpDescriptor($binding, $settings);
150
        }
151
152
        return $descriptor;
153
    }
154
155
    /**
156
     * @param string $binding
157
     * @return IDPSSODescriptor
158
     */
159
    protected function createIdpDescriptor(string $binding, SettingsInterface $settings)
160
    {
161
        $descriptor = new \SAML2\XML\md\IDPSSODescriptor();
162
        $descriptor->setProtocolSupportEnumeration([
163
            static::PROTOCOL,
164
        ]);
165
166
        if (property_exists($settings, 'wantsAuthnRequestsSigned')) {
167
            $descriptor->setWantAuthnRequestsSigned(
168
                $settings->wantsAuthnRequestsSigned
0 ignored issues
show
Bug introduced by
Accessing wantsAuthnRequestsSigned on the interface flipbox\saml\core\models\SettingsInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
169
            );
170
        }
171
172
        // SSO
173
        $ssoEndpoint = new EndpointType();
174
        $ssoEndpoint->setBinding($binding);
175
        $ssoEndpoint->setLocation(
176
            $settings->getDefaultLoginEndpoint()
177
        );
178
        $descriptor->setSingleSignOnService([
179
            $ssoEndpoint,
180
        ]);
181
182
        // SLO
183
        $this->addSloEndpoint(
184
            $descriptor,
185
            $settings
186
        );
187
188
        // todo add attributes from mapping
189
//        $attribute = new Attribute();
190
//        $attribute->setName('Username');
191
//        $descriptor->addAttribute(
192
//            $attribute
193
//        );
194
195
        return $descriptor;
196
    }
197
198
    /**
199
     * @param string $binding
200
     * @return SPSSODescriptor
201
     */
202
    protected function createSpDescriptor(string $binding, SettingsInterface $settings)
203
    {
204
205
        $descriptor = new SPSSODescriptor();
206
        $descriptor->setProtocolSupportEnumeration([
207
            static::PROTOCOL,
208
        ]);
209
210
        if (property_exists($settings, 'wantsSignedAssertions') &&
211
            is_bool($settings->wantsSignedAssertions)
0 ignored issues
show
Bug introduced by
Accessing wantsSignedAssertions on the interface flipbox\saml\core\models\SettingsInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
212
        ) {
213
            $descriptor->setWantAssertionsSigned(
214
                $settings->wantsSignedAssertions
0 ignored issues
show
Bug introduced by
Accessing wantsSignedAssertions on the interface flipbox\saml\core\models\SettingsInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
215
            );
216
        }
217
218
219
        // ACS
220
        $acsEndpoint = new IndexedEndpointType();
221
        $acsEndpoint->setIndex(1);
222
        $acsEndpoint->setBinding($binding);
223
        $acsEndpoint->setLocation(
224
            $settings->getDefaultLoginEndpoint()
225
        );
226
227
        $descriptor->setAssertionConsumerService([
228
            $acsEndpoint,
229
        ]);
230
231
        //SLO
232
        $this->addSloEndpoint(
233
            $descriptor,
234
            $settings
235
        );
236
237
        //todo add attribute consuming service
238
//        $attributeConsumingService = new AttributeConsumingService();
239
//        $attributeConsumingService->addRequestedAttribute($att = new RequestedAttribute());
240
//        $att->setName('username');
241
//        $descriptor->addAttributeConsumingService($attributeConsumingService);
242
243
        return $descriptor;
244
    }
245
246
    protected function addSloEndpoint(SSODescriptorType $descriptorType, SettingsInterface $settings)
247
    {
248
        $sloEndpointRedirect = new EndpointType();
249
        $sloEndpointRedirect->setBinding(
250
            Constants::BINDING_HTTP_REDIRECT
251
        );
252
        $sloEndpointRedirect->setLocation(
253
            $settings->getDefaultLogoutEndpoint()
254
        );
255
256
        $sloEndpointPost = new EndpointType();
257
        $sloEndpointPost->setBinding(
258
            Constants::BINDING_HTTP_POST
259
        );
260
        $sloEndpointPost->setLocation(
261
            $settings->getDefaultLogoutEndpoint()
262
        );
263
264
        $descriptorType->setSingleLogoutService([
265
            $sloEndpointRedirect,
266
            $sloEndpointPost,
267
        ]);
268
    }
269
270
    /**
271
     * @param SSODescriptorType $ssoDescriptor
272
     * @param KeyChainRecord $keyChainRecord
273
     */
274
    protected function setCertificate(
275
        SSODescriptorType $ssoDescriptor,
276
        KeyChainRecord $keyChainRecord,
277
        string $signOrEncrypt
278
    ) {
279
        /**
280
         * Validate use string
281
         */
282
        if (! in_array($signOrEncrypt, [
283
            self::SET_SIGNING,
284
            self::SET_ENCRYPTION,
285
        ])) {
286
            throw new \InvalidArgumentException('Sign or Encrypt argument can only be "signing" or "encrypt"');
287
        }
288
289
        /**
290
         * Create sub object
291
         */
292
        $keyDescriptor = new KeyDescriptor();
293
        $keyInfo = new KeyInfo();
294
        $x509Data = new X509Data();
295
        $x509Certificate = new X509Certificate();
296
297
        $keyInfo->addInfo($x509Data);
298
299
        $x509Certificate->setCertificate(
300
            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...
301
        );
302
303
        $x509Data->addData($x509Certificate);
304
        $keyDescriptor->setKeyInfo($keyInfo);
305
306
        $keyDescriptor->setUse($signOrEncrypt);
307
        $ssoDescriptor->addKeyDescriptor($keyDescriptor);
308
    }
309
310
    /**
311
     * @param SSODescriptorType
312
     * @param KeyChainRecord $keyChainRecord
313
     */
314
    protected function setSign(SSODescriptorType $ssoDescriptor, KeyChainRecord $keyChainRecord)
315
    {
316
        $this->setCertificate($ssoDescriptor, $keyChainRecord, static::SET_SIGNING);
317
    }
318
319
    /**
320
     * @param SSODescriptorType
321
     * @param KeyChainRecord $keyChainRecord
322
     */
323
    protected function setEncrypt(SSODescriptorType $ssoDescriptor, KeyChainRecord $keyChainRecord)
324
    {
325
        $this->setCertificate($ssoDescriptor, $keyChainRecord, static::SET_ENCRYPTION);
326
    }
327
}
328