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

src/services/Login.php (4 issues)

Severity

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
}