Completed
Push — master ( 524cff...dc4813 )
by
unknown
07:00
created

src/services/Login.php (6 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
 * Created by PhpStorm.
4
 * User: dsmrt
5
 * Date: 1/11/18
6
 * Time: 8:30 PM
7
 */
8
9
namespace flipbox\saml\sp\services;
10
11
12
use craft\base\Component;
13
use craft\elements\User;
14
use craft\models\UserGroup;
15
use flipbox\keychain\records\KeyChainRecord;
16
use flipbox\saml\core\exceptions\InvalidMessage;
17
use flipbox\saml\core\records\ProviderInterface;
18
use flipbox\saml\sp\helpers\UserHelper;
19
use flipbox\saml\sp\records\ProviderIdentityRecord;
20
use flipbox\saml\sp\Saml;
21
use LightSaml\Model\Assertion\Assertion;
22
use LightSaml\Model\Protocol\Response as SamlResponse;
23
use yii\base\Event;
24
use yii\base\UserException;
25
26
class Login extends Component
27
{
28
29
    const EVENT_RESPONSE_TO_USER = 'eventResponseToUser';
30
31
    /**
32
     * @param SamlResponse $response
33
     * @return \flipbox\saml\sp\records\ProviderIdentityRecord
34
     * @throws InvalidMessage
35
     * @throws UserException
36
     * @throws \Exception
37
     * @throws \Throwable
38
     * @throws \craft\errors\ElementNotFoundException
39
     * @throws \yii\base\Exception
40
     */
41
    public function login(SamlResponse $response)
42
    {
43
44
        $assertion = $this->getFirstAssertion($response);
45
        Saml::getInstance()->getResponse()->isValidTimeAssertion($assertion);
46
        Saml::getInstance()->getResponse()->isValidAssertion($assertion);
47
48
        /**
49
         * Sync User
50
         */
51
        $identity = $this->syncUser($response);
52
53
        /**
54
         * Log user in
55
         */
56
        if (! $this->loginUser($identity)) {
57
            throw new UserException("Unknown error while logging in.");
58
        }
59
60
        /**
61
         * User's successfully logged in so we can now set the lastLogin for the
62
         * provider identity and save it to the db.
63
         */
64
        $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...
65
        if (! Saml::getInstance()->getProviderIdentity()->save($identity)) {
66
            throw new UserException("Error while saving identity.");
67
        }
68
69
        return $identity;
70
71
    }
72
73
    protected function decryptAssertions(KeyChainRecord $keyChainRecord, \LightSaml\Model\Protocol\Response $response)
74
    {
75
        Saml::getInstance()->getResponse()->decryptAssertions(
76
            $response,
77
            $keyChainRecord
78
        );
79
    }
80
81
    /**
82
     * @param SamlResponse $response
83
     * @return Assertion
84
     * @throws InvalidMessage
85
     */
86
    public function getFirstAssertion(\LightSaml\Model\Protocol\Response $response)
87
    {
88
89
        $ownProvider = Saml::getInstance()->getProvider()->findOwn();
90
91
        if ($ownProvider->keychain && $response->getFirstEncryptedAssertion()) {
92
            $this->decryptAssertions(
93
                $ownProvider->keychain,
94
                $response
95
            );
96
        }
97
98
        $assertions = $response->getAllAssertions();
99
100
        if (! isset($assertions[0])) {
101
            throw new InvalidMessage("Invalid message. No assertions found in response.");
102
        }
103
        /**
104
         * Just grab the first one for now.
105
         */
106
        return $assertions[0];
107
    }
108
109
    /**
110
     * @param SamlResponse $response
111
     * @return \flipbox\saml\sp\records\ProviderIdentityRecord
112
     * @throws InvalidMessage
113
     * @throws UserException
114
     * @throws \Throwable
115
     * @throws \craft\errors\ElementNotFoundException
116
     * @throws \yii\base\Exception
117
     */
118
    protected function syncUser(\LightSaml\Model\Protocol\Response $response)
119
    {
120
121
        $assertion = $this->getFirstAssertion($response);
122
123
        /**
124
         * Get username from the NameID
125
         * @todo Give an option to map another attribute value to $username (like email)
126
         */
127
        $username = $assertion->getSubject()->getNameID()->getValue();
128
129
        $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
130
            $response->getIssuer()->getValue()
131
        );
132
133
        /**
134
         * Get Identity
135
         */
136
        $identity = $this->forceGetIdentity($username, $idpProvider);
137
138
        /**
139
         * Ger User
140
         */
141
        if (! $user = $identity->getUser()) {
142
            $user = $this->forceGetUser($username);
143
        }
144
145
        /**
146
         * Is User Active?
147
         */
148
        if (! UserHelper::isUserActive($user)) {
149
            if (! Saml::getInstance()->getSettings()->enableUsers) {
150
                throw new UserException('User access denied.');
151
            }
152
            UserHelper::enableUser($user);
153
        }
154
155
156
        /**
157
         *
158
         */
159
        $this->transformToUser($response, $user);
160
161
        /**
162
         * Before user save
163
         */
164
        $event = new Event();
165
        $event->sender = $response;
166
        $event->data = $user;
167
168
        $this->trigger(
169
            static::EVENT_RESPONSE_TO_USER,
170
            $event
171
        );
172
173
        if (! \Craft::$app->getElements()->saveElement($user)) {
174
            throw new UserException("User save failed.");
175
        }
176
177
        /**
178
         * Sync groups depending on the plugin setting.
179
         */
180
        if (Saml::getInstance()->getSettings()->syncGroups) {
181
            $this->syncUserGroupsByAssertion($user, $assertion);
182
        }
183
184
        /**
185
         * Get Session
186
         */
187
        $sessionIndex = null;
188
        if ($assertion->hasAnySessionIndex()) {
189
            $sessionIndex = $assertion->getFirstAuthnStatement()->getSessionIndex();
190
        }
191
192
        /**
193
         * Set Identity Properties
194
         */
195
        $identity->userId = $user->id;
0 ignored issues
show
The property userId 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...
196
        $identity->enabled = true;
0 ignored issues
show
The property enabled 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...
197
        $identity->sessionId = $sessionIndex;
0 ignored issues
show
The property sessionId 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...
198
        return $identity;
199
    }
200
201
    /**
202
     * @param $nameId
203
     * @param ProviderInterface $provider
204
     * @return ProviderIdentityRecord
205
     * @throws UserException
206
     */
207
    protected function forceGetIdentity($nameId, ProviderInterface $provider)
208
    {
209
210
        /** @var \flipbox\saml\sp\records\ProviderIdentityRecord $identity */
211
        if (! $identity = Saml::getInstance()->getProviderIdentity()->findByNameId(
212
            $nameId,
213
            $provider
214
        )) {
215
            if (! Saml::getInstance()->getSettings()->createUser) {
216
                throw new UserException("System doesn't have permission to create a new user.");
217
            }
218
219
            /**
220
             * Create the new identity if one wasn't found above.
221
             * Since we now have the user id, and we might not have above,
222
             * do this last.
223
             */
224
            $identity = new ProviderIdentityRecord([
225
                'providerId' => $provider->id,
0 ignored issues
show
Accessing id on the interface flipbox\saml\core\records\ProviderInterface 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...
226
                'nameId'     => $nameId,
227
            ]);
228
        }
229
230
        return $identity;
231
    }
232
233
    /**
234
     * @param $username
235
     * @return User
236
     * @throws UserException
237
     */
238
    protected function forceGetUser($username)
239
    {
240
241
        /**
242
         * Is there a user that exists already?
243
         */
244
        if ($user = $this->getUserByUsernameOrEmail($username)) {
245
            /**
246
             * System check for whether we are allowed merge with this this user
247
             */
248
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
249
                //don't continue
250
                throw new UserException(
251
                    sprintf(
252
                        "User (%s) already exists.",
253
                        $username
254
                    )
255
                );
256
            }
257
        } else {
258
            /**
259
             * New User
260
             */
261
            $user = new User([
262
                'username' => $username
263
            ]);
264
        }
265
266
        return $user;
267
    }
268
269
    /**
270
     * @param $emailOrUsername
271
     * @return User|null
272
     */
273
    protected function getUserByUsernameOrEmail($usernameOrEmail, bool $archived = false)
274
    {
275
276
        return User::find()
277
            ->where([
278
                'or',
279
                ['username' => $usernameOrEmail],
280
                ['email' => $usernameOrEmail]
281
            ])
282
            ->addSelect(['users.password', 'users.passwordResetRequired'])
283
            ->status(null)
284
            ->archived($archived)
285
            ->one();
286
    }
287
288
    /**
289
     * @param User $user
290
     * @param Assertion $assertion
291
     * @return bool
292
     * @throws UserException
293
     */
294
    protected function syncUserGroupsByAssertion(User $user, Assertion $assertion)
295
    {
296
        $groupNames = Saml::getInstance()->getSettings()->groupAttributeNames;
297
        $groups = [];
298
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
299
            /**
300
             * Is there a group name match?
301
             * Match the attribute name to the specified name in the plugin settings
302
             */
303
            if (in_array($attribute->getName(), $groupNames)) {
304
                /**
305
                 * Loop thru all of the attributes values because they could have multiple values.
306
                 * Example XML:
307
                 * <saml2:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
308
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
309
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">craft_admin</saml2:AttributeValue>
310
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
311
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">craft_member</saml2:AttributeValue>
312
                 * </saml2:Attribute>
313
                 */
314
315
                foreach ($attribute->getAllAttributeValues() as $value) {
316
                    if ($group = $this->findOrCreateUserGroup($value)) {
317
                        $groups[] = $group->id;
318
                    }
319
                }
320
321
            }
322
        }
323
        /**
324
         * just return if this is empty
325
         */
326
        if (empty($groups)) {
327
            return true;
328
        }
329
330
        return \Craft::$app->getUsers()->assignUserToGroups($user->id, $groups);
331
332
    }
333
334
    /**
335
     * @param SamlResponse $response
336
     * @param User $user
337
     * @return User
338
     */
339
    protected function transformToUser(\LightSaml\Model\Protocol\Response $response, User $user)
340
    {
341
        $assertion = $response->getFirstAssertion();
342
343
        $attributeMap = Saml::getInstance()->getSettings()->responseAttributeMap;
344
        /**
345
         * Loop thru attributes and set to the user
346
         */
347
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
348
            if (isset($attributeMap[$attribute->getName()])) {
349
                $craftProperty = $attributeMap[$attribute->getName()];
350
351
                //check if it exists as a property first
352
                if (property_exists($user, $craftProperty)) {
353
                    $user->{$craftProperty} = $attribute->getFirstAttributeValue();
354
                } else {
355
                    if (is_callable($craftProperty)) {
356
                        call_user_func($craftProperty, $user, $attribute);
357
                    }
358
                }
359
            }
360
        }
361
362
        return $user;
363
364
    }
365
366
    /**
367
     * @param $groupHandle
368
     * @return UserGroup
369
     * @throws UserException
370
     * @throws \craft\errors\WrongEditionException
371
     */
372
    protected function findOrCreateUserGroup($groupHandle): UserGroup
373
    {
374
375
        if (! $userGroup = \Craft::$app->getUserGroups()->getGroupByHandle($groupHandle)) {
376
            if (! \Craft::$app->getUserGroups()->saveGroup($userGroup = new UserGroup([
377
                'name'   => $groupHandle,
378
                'handle' => $groupHandle,
379
            ]))) {
380
                throw new UserException("Error saving new group {$groupHandle}");
381
            }
382
        }
383
384
        return $userGroup;
385
386
    }
387
388
    /**
389
     * @param ProviderIdentityRecord $identity
390
     * @return bool
391
     * @throws UserException
392
     * @throws \Throwable
393
     */
394
    protected function loginUser(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
395
    {
396
        if ($identity->getUser()->getStatus() !== User::STATUS_ACTIVE) {
397
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
398
                throw new UserException("Can't activate user.");
399
            }
400
        }
401
402
        if (\Craft::$app->getUser()->login(
403
            $identity->getUser(),
404
            /** @todo read session duration from the response */
405
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
406
        )) {
407
            $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...
408
        } else {
409
            throw new UserException("User login failed.");
410
        }
411
412
        return true;
413
    }
414
415
}