Issues (20)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/services/login/User.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 */
6
7
namespace flipbox\saml\sp\services\login;
8
9
use craft\base\Component;
10
use craft\base\Field;
11
use craft\elements\User as UserElement;
12
use craft\events\ElementEvent;
13
use craft\models\FieldLayout;
14
use flipbox\saml\core\exceptions\InvalidMessage;
15
use flipbox\saml\core\helpers\ProviderHelper;
16
use flipbox\saml\sp\helpers\UserHelper;
17
use flipbox\saml\sp\models\Settings;
18
use flipbox\saml\sp\records\ProviderIdentityRecord;
19
use flipbox\saml\sp\records\ProviderRecord;
20
use flipbox\saml\sp\Saml;
21
use SAML2\Response as SamlResponse;
22
use yii\base\UserException;
23
24
/**
25
 * Class User
26
 * @package flipbox\saml\sp\services
27
 */
28
class User extends Component
29
{
30
    use AssertionTrait;
31
32
    const EVENT_BEFORE_USER_SAVE = 'eventBeforeUserSave';
33
34
    /**
35
     * @var FieldLayout|null
36
     */
37
    private $fieldLayout;
38
    /**
39
     * @var Field[]
40
     */
41
    private $fields = [];
42
43
    /**
44
     * @param SamlResponse $response
45
     * @return UserElement
46
     * @throws InvalidMessage
47
     * @throws UserException
48
     */
49 3
    public function getByResponse(
50
        SamlResponse $response,
51
        ProviderRecord $serviceProvider,
52
        ProviderRecord $identityProvider,
53
        Settings $settings
54
    ) {
55
56
        $username = null;
57 3
58
        $nameIdOverride = $settings->nameIdAttributeOverride ?? $identityProvider->nameIdOverride;
0 ignored issues
show
The property nameIdOverride does not exist on object<flipbox\saml\sp\records\ProviderRecord>. 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...
59 3
60
        if ($nameIdOverride) {
61 3
            // use override
62
            foreach ($this->getAssertions(
63
                $response,
64
                $serviceProvider
65
            ) as $assertion) {
66
                $attributes = $assertion->getAttributes();
67
                if (isset($attributes[$nameIdOverride])) {
68
                    $attributeValue = $attributes[$nameIdOverride];
69
                    $username = $this->getAttributeValue($attributeValue);
70
                }
71
            }
72
        } else {
73
            // use nameid
74
            $assertion = $this->getFirstAssertion($response, $serviceProvider);
75 3
76
            if (! $assertion->getNameId()) {
77 3
                throw new InvalidMessage('Name ID is missing.');
78 3
            }
79
            $username = $assertion->getNameId()->getValue();
80 3
81
            Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
82 3
        }
83
84
        return $this->find($username);
85 3
    }
86
87
    /**
88
     * @param ProviderIdentityRecord $identity
89
     * @return bool
90
     * @throws UserException
91
     * @throws \Throwable
92
     */
93
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
94
    {
95
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
96
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
97
                throw new UserException("Can't activate user.");
98
            }
99
        }
100
101
        if (\Craft::$app->getUser()->login(
102
            $identity->getUser(),
103
            /**
104
             * @todo read session duration from the response
105
             */
106
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
107
        )
108
        ) {
109
            $identity->lastLoginDate = new \DateTime();
0 ignored issues
show
The property lastLoginDate does not exist on object<flipbox\saml\sp\r...ProviderIdentityRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write 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.");
        }
    }

}

Since the property has write access only, you can use the @property-write 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...
110
        } else {
111
            throw new UserException("User login failed.");
112
        }
113
114
        return true;
115
    }
116
117
    /**
118
     * @param UserElement $user
119
     * @param SamlResponse $response
120
     * @param ProviderRecord $idp
121
     * @param Settings $settings
122
     * @throws UserException
123
     * @throws \Throwable
124
     * @throws \craft\errors\ElementNotFoundException
125
     * @throws \yii\base\Exception
126
     */
127
    public function sync(
128
        UserElement $user,
129
        SamlResponse $response,
130
        ProviderRecord $idp,
131
        ProviderRecord $sp,
132
        Settings $settings
133
    ) {
134
135
        // enable and transform the user
136
        $this->construct(
137
            $user,
138
            $response,
139
            $idp,
140
            $sp,
141
            $settings
142
        );
143
144
        // Save
145
        $this->save($user);
146
    }
147
148
    /**
149
     * @param UserElement $user
150
     * @return bool
151
     * @throws UserException
152
     * @throws \Throwable
153
     * @throws \craft\errors\ElementNotFoundException
154
     * @throws \yii\base\Exception
155
     */
156
    protected function save(UserElement $user)
157
    {
158
159
        $event = new ElementEvent();
160
        $event->element = $user;
161
        $event->isNew = !$user->id;
162
163
        $this->trigger(
164
            static::EVENT_BEFORE_USER_SAVE,
165
            $event
166
        );
167
168
        if (! \Craft::$app->getElements()->saveElement($user)) {
169
            Saml::error(
170
                'User save failed: ' . \json_encode($user->getErrors())
171
            );
172
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
173
        }
174
175
        return true;
176
    }
177
178
    /**
179
     * Response Based Methods
180
     */
181
182
    /**
183
     * @param UserElement $user
184
     * @param SamlResponse $response
185
     * @param ProviderRecord $idp
186
     * @param Settings $settings
187
     * @throws UserException
188
     * @throws \Throwable
189
     */
190
    protected function construct(
191 3
        UserElement $user,
192
        SamlResponse $response,
193
        ProviderRecord $idp,
194
        ProviderRecord $sp,
195
        Settings $settings
196
    ) {
197
        /**
198
         * Is User Active?
199
         */
200
        if (! UserHelper::isUserActive($user)) {
201 3
            if (! $settings->enableUsers) {
202
                throw new UserException('User access denied.');
203
            }
204
            UserHelper::enableUser($user);
205
        }
206
207
        foreach ($this->getAssertions($response, $sp) as $assertion) {
208 3
            $hasAttributes = count($assertion->getAttributes()) > 0;
209 3
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
210 3
            if ($hasAttributes) {
211 3
                $this->transform(
212 3
                    $user,
213 3
                    $response,
214 1
                    $idp,
215 1
                    $sp,
216 1
                    $settings
217 1
                );
218
            } else {
219
                /**
220
                 * There doesn't seem to be any attribute statements.
221
                 * Try and use username for the email and move on.
222
                 */
223
                Saml::warning(
224
                    'No attribute statements found! Trying to assign username as the email.'
225
                );
226
                $user->email = $user->email ?: $user->username;
227
            }
228
        }
229
    }
230 3
231
    /**
232
     * @param UserElement $user
233
     * @param SamlResponse $response
234
     * @return UserElement
235
     */
236
    protected function transform(
237 3
        UserElement $user,
238
        SamlResponse $response,
239
        ProviderRecord $idp,
240
        ProviderRecord $sp,
241
        Settings $settings
242
    ) {
243
244
        foreach ($this->getAssertions($response, $sp) as $assertion) {
245 3
            /**
246
             * Check the provider first
247
             */
248
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
249 3
                $idp
250 3
            ) ?:
251
                $settings->responseAttributeMap;
252 3
253
            Saml::debug('Attribute Map: ' . \json_encode($attributeMap));
254 3
255
            /**
256
             * Loop thru attributes and set to the user
257
             */
258
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
259 3
                Saml::debug('Attributes: ' . $attributeName . ' ' . \json_encode($attributeValue));
260 3
                if (isset($attributeMap[$attributeName])) {
261 3
                    $craftProperty = $attributeMap[$attributeName];
262 3
                    $this->assignProperty(
263 3
                        $user,
264 3
                        $attributeName,
265 1
                        $attributeValue,
266 1
                        $craftProperty
267 1
                    );
268
                } else {
269
                    Saml::debug('No match for: ' . $attributeName);
270 3
                }
271
            }
272
        }
273
274
        return $user;
275 3
    }
276
277
    /**
278
     * @param UserElement $user
279
     * @param $attributeName
280
     * @param $attributeValue
281
     * @param $craftProperty
282
     */
283
    protected function assignProperty(
284 3
        UserElement $user,
285
        $attributeName,
286
        $attributeValue,
287
        $craftProperty
288
    ) {
289
290
        $originalValues = $attributeValue;
291 3
        if (is_array($attributeValue)) {
292 3
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
293 3
        }
294
295
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
296 3
            Saml::debug(
297 3
                sprintf(
298 3
                    'Attribute %s is scalar and should set value "%s" to user->%s',
299 3
                    $attributeName,
300 1
                    $attributeValue,
301 1
                    $craftProperty
302 1
                )
303
            );
304
305
            $this->setSimpleProperty($user, $craftProperty, $attributeValue);
306 3
        } elseif (is_callable($craftProperty)) {
307
            Saml::debug(
308
                sprintf(
309
                    'Attribute %s is handled with a callable.',
310
                    $attributeName
311
                )
312
            );
313
314
            call_user_func($craftProperty, $user, [
315
                $attributeName => $originalValues,
316
            ]);
317
        }
318
    }
319 3
320
    /**
321
     * @param UserElement $user
322
     * @return Field|null
323
     */
324
    protected function getFieldLayoutField(UserElement $user, $fieldHandle)
325 3
    {
326
        if (! $this->fieldLayout) {
327 3
            $this->fieldLayout = $user->getFieldLayout();
328 3
        }
329
        if (is_null($this->fieldLayout)) {
330 3
            return null;
331
        }
332
333
        if (! isset($this->fields[$fieldHandle])) {
334 3
            $this->fields[$fieldHandle] = $this->fieldLayout->getFieldByHandle($fieldHandle);
335 3
        }
336
337
338
        return $this->fields[$fieldHandle];
339 3
    }
340
341
    /**
342
     * @param UserElement $user
343
     * @param string $name
344
     * @param mixed $value
345
     */
346
    private function setSimpleProperty(UserElement $user, $name, $value)
347 3
    {
348
        $field = $this->getFieldLayoutField($user, $name);
349 3
350
        Saml::info(
351 3
            sprintf(
352 3
                '%s as %s. Is Field? %s',
353 3
                $name,
354 1
                $value,
355 1
                $field instanceof Field ? $field->id : 'Nope'
356 3
            )
357
        );
358
359
        if (! is_null($field)) {
360 3
            $user->setFieldValue($name, $value);
361
        } else {
362
            $user->{$name} = $value;
363 3
        }
364
    }
365 3
366
    /**************************************************
367
     * Craft User Methods
368
     **************************************************/
369
370
    /**
371
     * @param $username
372
     * @return UserElement
373
     * @throws UserException
374
     */
375
    protected function find($username)
376 3
    {
377
        return $this->forceGet($username);
378 3
    }
379
380
    /**
381
     * @param $username
382
     * @return UserElement
383
     * @throws UserException
384
     */
385
    protected function forceGet($username)
386 3
    {
387
388
        /**
389
         * Is there a user that exists already?
390
         */
391
        if (!($user = $this->getByUsernameOrEmail($username))) {
392 3
            // Should we create a new user? what's the setting say?
393
            if (! Saml::getInstance()->getSettings()->createUser) {
394 3
                throw new UserException("System doesn't have permission to create a new user.");
395
            }
396
397
            // new user!
398
            $user = new UserElement(
399 3
                [
400
                    'username' => $username,
401 3
                ]
402
            );
403
        }
404
405
        return $user;
406 3
    }
407
408
    /**
409
     * @param $usernameOrEmail
410
     * @param bool $archived
411
     * @return array|bool|\craft\base\ElementInterface|UserElement|null
412
     */
413
    protected function getByUsernameOrEmail($usernameOrEmail, $archived = false)
414 3
    {
415
416
        return UserElement::find()
417 3
            ->where(
418 3
                [
419
                    'or',
420 3
                    ['username' => $usernameOrEmail],
421 3
                    ['email' => $usernameOrEmail],
422 3
                ]
423
            )
424
            ->status(null)
425 3
            ->archived($archived)
426 3
            ->one();
427 3
    }
428
429
    private function getAttributeValue($attributeValue)
430
    {
431
432
        if (is_array($attributeValue)) {
433
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
434
        }
435
436
        return $attributeValue;
437
    }
438
}
439