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

src/services/Login.php (4 issues)

mismatching argument types.

Documentation Minor

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\sp\records\ProviderIdentityRecord;
18
use flipbox\saml\sp\Saml;
19
use LightSaml\Model\Assertion\Assertion;
20
use LightSaml\Model\Protocol\Response as SamlResponse;
21
use yii\base\Event;
22
use yii\base\UserException;
23
24
class Login extends Component
25
{
26
27
    const EVENT_RESPONSE_TO_USER = 'eventResponseToUser';
28
29
    /**
30
     * @param SamlResponse $response
31
     * @return \flipbox\saml\sp\records\ProviderIdentityRecord
32
     * @throws InvalidMessage
33
     * @throws UserException
34
     * @throws \Exception
35
     * @throws \Throwable
36
     * @throws \craft\errors\ElementNotFoundException
37
     * @throws \yii\base\Exception
38
     */
39
    public function login(SamlResponse $response)
40
    {
41
42
        $assertion = $this->getFirstAssertion($response);
43
        Saml::getInstance()->getResponse()->isValidTimeAssertion($assertion);
44
        Saml::getInstance()->getResponse()->isValidAssertion($assertion);
45
46
        /**
47
         * Sync User
48
         */
49
        $identity = $this->syncUser($response);
50
51
        /**
52
         * Log user in
53
         */
54
        if (! $this->loginUser($identity)) {
55
            throw new UserException("Unknown error while logging in.");
56
        }
57
58
        /**
59
         * User's successfully logged in so we can now set the lastLogin for the
60
         * provider identity and save it to the db.
61
         */
62
        $identity->lastLoginDate = new \DateTime();
63
        if (! Saml::getInstance()->getProviderIdentity()->save($identity)) {
64
            throw new UserException("Error while saving identity.");
65
        }
66
67
        return $identity;
68
69
    }
70
71
    protected function decryptAssertions(KeyChainRecord $keyChainRecord, \LightSaml\Model\Protocol\Response $response)
72
    {
73
        Saml::getInstance()->getResponse()->decryptAssertions(
74
            $response,
75
            $keyChainRecord
76
        );
77
    }
78
79
    /**
80
     * @param SamlResponse $response
81
     * @return Assertion
82
     * @throws InvalidMessage
83
     */
84
    public function getFirstAssertion(\LightSaml\Model\Protocol\Response $response)
85
    {
86
87
        $ownProvider = Saml::getInstance()->getProvider()->findOwn();
88
89
        if ($ownProvider->keychain && $response->getFirstEncryptedAssertion()) {
90
            $this->decryptAssertions(
91
                $ownProvider->keychain,
92
                $response
93
            );
94
        }
95
96
        $assertions = $response->getAllAssertions();
97
98
        if (! isset($assertions[0])) {
99
            throw new InvalidMessage("Invalid message. No assertions found in response.");
100
        }
101
        /**
102
         * Just grab the first one for now.
103
         */
104
        return $assertions[0];
105
    }
106
107
    /**
108
     * @param SamlResponse $response
109
     * @return \flipbox\saml\sp\records\ProviderIdentityRecord
110
     * @throws InvalidMessage
111
     * @throws UserException
112
     * @throws \Throwable
113
     * @throws \craft\errors\ElementNotFoundException
114
     * @throws \yii\base\Exception
115
     */
116
    protected function syncUser(\LightSaml\Model\Protocol\Response $response)
117
    {
118
119
        $assertion = $this->getFirstAssertion($response);
120
121
        /**
122
         * Get username from the NameID
123
         * @todo Give an option to map another attribute value to $username (like email)
124
         */
125
        $nameId = $username = $assertion->getSubject()->getNameID()->getValue();
126
127
        $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
128
            $response->getIssuer()->getValue()
129
        );
130
131
        /** @var \flipbox\saml\sp\records\ProviderIdentityRecord $identity */
132
        if (! $identity = Saml::getInstance()->getProviderIdentity()->findByNameId(
133
            $nameId,
134
            $idpProvider
135
        )) {
136
            if (! Saml::getInstance()->getSettings()->createUser) {
137
                throw new UserException("System doesn't have permission to create a new user.");
138
            }
139
        }
140
141
        /**
142
         * Is there a user that exists already?
143
         */
144
        if ($user = $this->getUserByUsernameOrEmail($username)) {
145
            /**
146
             * System check for whether we are allowed merge with this this user
147
             */
148
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
149
                //don't continue
150
                throw new UserException(
151
                    sprintf(
152
                        "User (%s) already exists.",
153
                        $username
154
                    )
155
                );
156
            }
157
        } else {
158
            /**
159
             *
160
             */
161
            if ($user = $this->getUserByUsernameOrEmail($username, true)) {
162
163
                /**
164
                 * System check for whether we are allowed merge with this this user
165
                 */
166
                if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
167
                    //don't continue
168
                    throw new UserException(
169
                        sprintf(
170
                            "User (%s) already exists.",
171
                            $username
172
                        )
173
                    );
174
                }
175
            } else {
176
                /**
177
                 * New User
178
                 */
179
                $user = new User([
180
                    'username' => $username
181
                ]);
182
183
            }
184
        }
185
186
        if (! $this->isUserActive($user)) {
0 ignored issues
show
$user is of type array|object<craft\base\ElementInterface>, but the function expects a object<craft\elements\User>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
187
            if (! Saml::getInstance()->getSettings()->enableUsers) {
188
                throw new UserException('User access denied.');
189
            }
190
            $this->enableUser($user);
0 ignored issues
show
$user is of type array|object<craft\base\ElementInterface>, but the function expects a object<craft\elements\User>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
191
        }
192
193
194
        $this->transformToUser($response, $user);
0 ignored issues
show
$user is of type array|object<craft\base\ElementInterface>, but the function expects a object<craft\elements\User>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
195
196
        /**
197
         * After event for Metadata creation
198
         */
199
        $event = new Event();
200
        $event->sender = $response;
201
        $event->data = $user;
202
203
        $this->trigger(
204
            static::EVENT_RESPONSE_TO_USER,
205
            $event
206
        );
207
208
        if (! \Craft::$app->getElements()->saveElement($user)) {
209
            throw new UserException("User save failed.");
210
        }
211
212
        /**
213
         * Sync groups depending on the plugin setting.
214
         */
215
        if (Saml::getInstance()->getSettings()->syncGroups) {
216
            $this->syncUserGroupsByAssertion($user, $assertion);
0 ignored issues
show
$user is of type array|object<craft\base\ElementInterface>, but the function expects a object<craft\elements\User>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
217
        }
218
219
        $sessionIndex = null;
220
        if ($assertion->hasAnySessionIndex()) {
221
            $sessionIndex = $assertion->getFirstAuthnStatement()->getSessionIndex();
222
        }
223
224
        /**
225
         * Create the new identity if one wasn't found above.
226
         * Since we now have the user id, and we might not have above,
227
         * do this last.
228
         */
229
        if (! $identity) {
230
            $identity = new ProviderIdentityRecord([
231
                'providerId' => $idpProvider->id,
232
                'nameId'     => $username,
233
                'userId'     => $user->id,
234
            ]);
235
        }
236
237
        $identity->enabled = true;
238
        $identity->sessionId = $sessionIndex;
239
        return $identity;
240
    }
241
242
    /**
243
     * @param $emailOrUsername
244
     * @return array|\craft\base\ElementInterface|User|null
245
     */
246
    protected function getUserByUsernameOrEmail($usernameOrEmail, bool $archived = false)
247
    {
248
249
        return User::find()
250
            ->where([
251
                'or',
252
                ['username' => $usernameOrEmail],
253
                ['email' => $usernameOrEmail]
254
            ])
255
            ->addSelect(['users.password', 'users.passwordResetRequired'])
256
            ->status(null)
257
            ->archived($archived)
258
            ->one();
259
    }
260
261
    /**
262
     * @param User $user
263
     * @throws \Throwable
264
     */
265
    protected function enableUser(User $user)
266
    {
267
        if ($this->isUserSuspended($user)) {
268
            \Craft::$app->getUsers()->unsuspendUser($user);
269
        }
270
271
        if ($this->isUserLocked($user)) {
272
            \Craft::$app->getUsers()->unlockUser($user);
273
        }
274
275
        if (! $user->enabled) {
276
            $user->enabled = true;
277
        }
278
279
        if ($user->archived) {
280
            $user->archived = false;
281
        }
282
283
        if (! $this->isUserActive($user)) {
284
            \Craft::$app->getUsers()->activateUser($user);
285
        }
286
    }
287
288
    /**
289
     * @param User $user
290
     * @return bool
291
     */
292
    protected function isUserPending(User $user)
293
    {
294
        return false === $this->isUserActive($user) &&
295
            $user->getStatus() === User::STATUS_PENDING;
296
    }
297
298
    /**
299
     * @param User $user
300
     * @return bool
301
     */
302
    protected function isUserArchived(User $user)
303
    {
304
        return false === $this->isUserActive($user) &&
305
            $user->getStatus() === User::STATUS_ARCHIVED;
306
    }
307
308
    /**
309
     * @param User $user
310
     * @return bool
311
     */
312
    protected function isUserLocked(User $user)
313
    {
314
        return false === $this->isUserActive($user) &&
315
            $user->getStatus() === User::STATUS_LOCKED;
316
    }
317
318
    /**
319
     * @param User $user
320
     * @return bool
321
     */
322
    protected function isUserSuspended(User $user)
323
    {
324
        return false === $this->isUserActive($user) &&
325
            $user->getStatus() === User::STATUS_SUSPENDED;
326
    }
327
328
    /**
329
     * @param User $user
330
     * @return bool
331
     */
332
    protected function isUserActive(User $user)
333
    {
334
        return $user->getStatus() === User::STATUS_ACTIVE;
335
    }
336
337
    /**
338
     * @param User $user
339
     * @param Assertion $assertion
340
     * @return bool
341
     * @throws UserException
342
     */
343
    protected function syncUserGroupsByAssertion(User $user, Assertion $assertion)
344
    {
345
        $groupNames = Saml::getInstance()->getSettings()->groupAttributeNames;
346
        $groups = [];
347
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
348
            /**
349
             * Is there a group name match?
350
             * Match the attribute name to the specified name in the plugin settings
351
             */
352
            if (in_array($attribute->getName(), $groupNames)) {
353
                /**
354
                 * Loop thru all of the attributes values because they could have multiple values.
355
                 * Example XML:
356
                 * <saml2:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
357
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
358
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">craft_admin</saml2:AttributeValue>
359
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
360
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">craft_member</saml2:AttributeValue>
361
                 * </saml2:Attribute>
362
                 */
363
364
                foreach ($attribute->getAllAttributeValues() as $value) {
365
                    if ($group = $this->findOrCreateUserGroup($value)) {
366
                        $groups[] = $group->id;
367
                    }
368
                }
369
370
            }
371
        }
372
        /**
373
         * just return if this is empty
374
         */
375
        if (empty($groups)) {
376
            return true;
377
        }
378
379
        return \Craft::$app->getUsers()->assignUserToGroups($user->id, $groups);
380
381
    }
382
383
384
    /**
385
     * @param SamlResponse $response
386
     * @param User $user
387
     * @return User
388
     */
389
    protected function transformToUser(\LightSaml\Model\Protocol\Response $response, User $user)
390
    {
391
        $assertion = $response->getFirstAssertion();
392
393
        $attributeMap = Saml::getInstance()->getSettings()->responseAttributeMap;
394
        /**
395
         * Loop thru attributes and set to the user
396
         */
397
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
398
            if (isset($attributeMap[$attribute->getName()])) {
399
                $craftProperty = $attributeMap[$attribute->getName()];
400
401
                //check if it exists as a property first
402
                if (property_exists($user, $craftProperty)) {
403
                    $user->{$craftProperty} = $attribute->getFirstAttributeValue();
404
                } else {
405
                    if (is_callable($craftProperty)) {
406
                        call_user_func($craftProperty, $user, $attribute);
407
                    }
408
                }
409
            }
410
        }
411
412
        return $user;
413
414
    }
415
416
    /**
417
     * @param $groupHandle
418
     * @return UserGroup
419
     * @throws UserException
420
     * @throws \craft\errors\WrongEditionException
421
     */
422
    protected function findOrCreateUserGroup($groupHandle): UserGroup
423
    {
424
425
        if (! $userGroup = \Craft::$app->getUserGroups()->getGroupByHandle($groupHandle)) {
426
            if (! \Craft::$app->getUserGroups()->saveGroup($userGroup = new UserGroup([
427
                'name'   => $groupHandle,
428
                'handle' => $groupHandle,
429
            ]))) {
430
                throw new UserException("Error saving new group {$groupHandle}");
431
            }
432
        }
433
434
        return $userGroup;
435
436
    }
437
438
    /**
439
     * @param ProviderIdentityRecord $identity
440
     * @return bool
441
     * @throws UserException
442
     * @throws \Throwable
443
     */
444
    protected function loginUser(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
445
    {
446
        if ($identity->getUser()->getStatus() !== User::STATUS_ACTIVE) {
447
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
448
                throw new UserException("Can't activate user.");
449
            }
450
        }
451
452
        if (\Craft::$app->getUser()->login(
453
            $identity->getUser(),
454
            /** @todo read session duration from the response */
455
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
456
        )) {
457
            $identity->lastLoginDate = new \DateTime();
458
        } else {
459
            throw new UserException("User login failed.");
460
        }
461
462
        return true;
463
    }
464
465
}