Completed
Push — master ( 05a9d8...53f93c )
by Damien
11s
created

Login::getUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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
use craft\base\Component;
12
use craft\elements\User;
13
use craft\helpers\StringHelper;
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
    /**
30
     * Use before or after now
31
     * @deprecated
32
     */
33
    const EVENT_RESPONSE_TO_USER = 'eventResponseToUser';
34
    const EVENT_BEFORE_RESPONSE_TO_USER = 'eventBeforeResponseToUser';
35
    const EVENT_AFTER_RESPONSE_TO_USER = 'eventAfterResponseToUser';
36
37
    protected $isAssertionDecrypted = false;
38
39
    /**
40
     * @param SamlResponse $response
41
     * @return \flipbox\saml\sp\records\ProviderIdentityRecord
42
     * @throws InvalidMessage
43
     * @throws UserException
44
     * @throws \Exception
45
     * @throws \Throwable
46
     * @throws \craft\errors\ElementNotFoundException
47
     * @throws \yii\base\Exception
48
     */
49
    public function login(SamlResponse $response)
50
    {
51
52
        $assertion = $this->getFirstAssertion($response);
53
        Saml::getInstance()->getResponse()->isValidTimeAssertion($assertion);
54
        Saml::getInstance()->getResponse()->isValidAssertion($assertion);
55
56
        /**
57
         * Get User
58
         */
59
        $user = $this->getUserByResponse($response);
60
61
        /**
62
         * Sync User
63
         */
64
        $this->syncUser($user, $response);
65
66
        /**
67
         * Get Identity
68
         */
69
        $identity = $this->getIdentityByUserAndResponse($user, $response);
70
71
        /**
72
         * Log user in
73
         */
74
        if (! $this->loginUser($identity)) {
75
            throw new UserException("Unknown error while logging in.");
76
        }
77
78
        /**
79
         * User's successfully logged in so we can now set the lastLogin for the
80
         * provider identity and save it to the db.
81
         */
82
        $identity->lastLoginDate = new \DateTime();
0 ignored issues
show
Documentation introduced by
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...
83
        if (! Saml::getInstance()->getProviderIdentity()->save($identity)) {
84
            throw new UserException("Error while saving identity.");
85
        }
86
87
        return $identity;
88
    }
89
90
    /**
91
     * @param KeyChainRecord $keyChainRecord
92
     * @param SamlResponse $response
93
     */
94
    protected function decryptAssertions(KeyChainRecord $keyChainRecord, \LightSaml\Model\Protocol\Response $response)
95
    {
96
        Saml::getInstance()->getResponse()->decryptAssertions(
97
            $response,
98
            $keyChainRecord
99
        );
100
    }
101
102
    /**
103
     * @param SamlResponse $response
104
     * @return Assertion
105
     * @throws InvalidMessage
106
     */
107
    public function getFirstAssertion(\LightSaml\Model\Protocol\Response $response)
108
    {
109
110
        $ownProvider = Saml::getInstance()->getProvider()->findOwn();
111
112
        if ($ownProvider->keychain &&
113
            $response->getFirstEncryptedAssertion() &&
114
            $this->isAssertionDecrypted === false
115
        ) {
116
            $this->decryptAssertions(
117
                $ownProvider->keychain,
118
                $response
119
            );
120
            /**
121
             * try to only do this once
122
             */
123
            $this->isAssertionDecrypted = true;
124
        }
125
126
        $assertions = $response->getAllAssertions();
127
128
        if (! isset($assertions[0])) {
129
            throw new InvalidMessage("Invalid message. No assertions found in response.");
130
        }
131
        /**
132
         * Just grab the first one for now.
133
         */
134
        return $assertions[0];
135
    }
136
137
    /**
138
     * @param SamlResponse $response
139
     * @return User
140
     * @throws InvalidMessage
141
     * @throws UserException
142
     * @throws \Throwable
143
     */
144
    protected function getUserByResponse(\LightSaml\Model\Protocol\Response $response)
145
    {
146
147
        $assertion = $this->getFirstAssertion($response);
148
149
        /**
150
         * Get username from the NameID
151
         *
152
         * @todo Give an option to map another attribute value to $username (like email)
153
         */
154
        $username = $assertion->getSubject()->getNameID()->getValue();
155
156
        return $this->getUser($username);
157
    }
158
159
    /**
160
     * @param User $user
161
     * @param SamlResponse $response
162
     * @throws UserException
163
     * @throws \Throwable
164
     * @throws \craft\errors\ElementNotFoundException
165
     * @throws \yii\base\Exception
166
     */
167
    protected function syncUser(User $user, \LightSaml\Model\Protocol\Response $response)
168
    {
169
        /**
170
         * Before user save
171
         */
172
        $event = new Event();
173
        $event->sender = $response;
174
        $event->data = $user;
175
176
        $this->trigger(
177
            static::EVENT_BEFORE_RESPONSE_TO_USER,
178
            $event
179
        );
180
181
        /**
182
         * enable and transform the user
183
         */
184
        $this->constructUser($user, $response);
185
186
        /**
187
         * Save
188
         */
189
        $this->saveUser($user);
190
191
        /**
192
         * Sync groups depending on the plugin setting.
193
         */
194
        $this->syncUserGroupsByAssertion($user, $this->getFirstAssertion($response));
195
196
        /**
197
         * after user save
198
         */
199
        $event = new Event();
200
        $event->sender = $response;
201
        $event->data = $user;
202
203
        $this->trigger(
204
            static::EVENT_AFTER_RESPONSE_TO_USER,
205
            $event
206
        );
207
    }
208
209
    /**
210
     * @param User $user
211
     * @param SamlResponse $response
212
     * @return ProviderIdentityRecord
213
     * @throws UserException
214
     */
215
    protected function getIdentityByUserAndResponse(User $user, \LightSaml\Model\Protocol\Response $response)
216
    {
217
218
        $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
219
            $response->getIssuer()->getValue()
220
        );
221
        /**
222
         * Get Identity
223
         */
224
        $identity = $this->forceGetIdentity($user->username, $idpProvider);
225
226
        /**
227
         * Get Session
228
         */
229
        $sessionIndex = null;
230
        if ($response->getFirstAssertion()->hasAnySessionIndex()) {
231
            $sessionIndex = $response->getFirstAssertion()->getFirstAuthnStatement()->getSessionIndex();
232
        }
233
234
        /**
235
         * Set Identity Properties
236
         */
237
        $identity->userId = $user->id;
238
        $identity->enabled = true;
239
        $identity->sessionId = $sessionIndex;
240
        return $identity;
241
    }
242
243
    /**
244
     * @param User $user
245
     * @return bool
246
     * @throws UserException
247
     * @throws \Throwable
248
     * @throws \craft\errors\ElementNotFoundException
249
     * @throws \yii\base\Exception
250
     */
251
    protected function saveUser(User $user)
252
    {
253
        if (! \Craft::$app->getElements()->saveElement($user)) {
254
            Saml::error(
255
                'User save failed: ' . json_encode($user->getErrors())
256
            );
257
            throw new UserException("User save failed: " . json_encode($user->getErrors()));
258
        }
259
260
        return true;
261
    }
262
263
    /**
264
     * @param $username
265
     * @return User
266
     * @throws UserException
267
     */
268
    protected function getUser($username)
269
    {
270
        return $this->forceGetUser($username);
271
    }
272
273
    /**
274
     * @param User $user
275
     * @param SamlResponse $response
276
     * @throws UserException
277
     * @throws \Throwable
278
     */
279
    protected function constructUser(User $user, \LightSaml\Model\Protocol\Response $response)
280
    {
281
        /**
282
         * Is User Active?
283
         */
284
        if (! UserHelper::isUserActive($user)) {
285
            if (! Saml::getInstance()->getSettings()->enableUsers) {
286
                throw new UserException('User access denied.');
287
            }
288
            UserHelper::enableUser($user);
289
        }
290
291
        $assertion = $this->getFirstAssertion($response);
292
293
        if ($assertion->getFirstAttributeStatement()) {
294
            /**
295
             *
296
             */
297
            $this->transformToUser($response, $user);
298
        } else {
299
300
            /**
301
             * There doesn't seem to be any attribute statements.
302
             * Try and use username for the email and move on.
303
             */
304
            \Craft::warning(
305
                'No attribute statements found! Trying to assign username as the email.',
306
                Saml::getInstance()->getHandle()
307
            );
308
            $user->email = $user->username;
309
        }
310
    }
311
312
    /**
313
     * @param string $nameId
314
     * @param ProviderInterface $provider
315
     * @return ProviderIdentityRecord
316
     * @throws UserException
317
     */
318
    protected function forceGetIdentity($nameId, ProviderInterface $provider)
319
    {
320
321
        /**
322
         * @var \flipbox\saml\sp\records\ProviderIdentityRecord $identity
323
         */
324
        if (! $identity = Saml::getInstance()->getProviderIdentity()->findByNameId(
325
            $nameId,
326
            $provider
327
        )
328
        ) {
329
            if (! Saml::getInstance()->getSettings()->createUser) {
330
                throw new UserException("System doesn't have permission to create a new user.");
331
            }
332
333
            /**
334
             * Create the new identity if one wasn't found above.
335
             * Since we now have the user id, and we might not have above,
336
             * do this last.
337
             */
338
            $identity = new ProviderIdentityRecord(
339
                [
340
                    'providerId' => $provider->id,
0 ignored issues
show
Bug introduced by
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...
341
                    'nameId'     => $nameId,
342
                ]
343
            );
344
        }
345
346
        return $identity;
347
    }
348
349
    /**
350
     * @param $username
351
     * @return User
352
     * @throws UserException
353
     */
354
    protected function forceGetUser($username)
355
    {
356
357
        /**
358
         * Is there a user that exists already?
359
         */
360
        if ($user = $this->getUserByUsernameOrEmail($username)) {
361
            /**
362
             * System check for whether we are allowed merge with this this user
363
             */
364
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
365
                //don't continue
366
                throw new UserException(
367
                    sprintf(
368
                        "User (%s) already exists.",
369
                        $username
370
                    )
371
                );
372
            }
373
        } else {
374
            /**
375
             * New User
376
             */
377
            $user = new User(
378
                [
379
                    'username' => $username
380
                ]
381
            );
382
        }
383
384
        return $user;
385
    }
386
387
    /**
388
     * @param $emailOrUsername
389
     * @return User|null
390
     */
391
    protected function getUserByUsernameOrEmail($usernameOrEmail, bool $archived = false)
392
    {
393
394
        return User::find()
395
            ->where(
396
                [
397
                    'or',
398
                    ['username' => $usernameOrEmail],
399
                    ['email' => $usernameOrEmail]
400
                ]
401
            )
402
            ->addSelect(['users.password', 'users.passwordResetRequired'])
403
            ->status(null)
404
            ->archived($archived)
405
            ->one();
406
    }
407
408
    /**
409
     * @param User $user
410
     * @param Assertion $assertion
411
     * @return bool
412
     * @throws UserException
413
     */
414
    protected function syncUserGroupsByAssertion(User $user, Assertion $assertion)
415
    {
416
        /**
417
         * Nothing to do, move on
418
         */
419
        if (false === Saml::getInstance()->getSettings()->syncGroups) {
420
            return true;
421
        }
422
423
        $groupNames = Saml::getInstance()->getSettings()->groupAttributeNames;
424
        $groups = [];
425
        /**
426
         * Make sure there is an attribute statement
427
         */
428
        if (! $assertion->getFirstAttributeStatement()) {
429
            Saml::debug(
430
                'No attribute statement found, moving on.'
431
            );
432
            return true;
433
        }
434
435
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
436
            Saml::debug(
437
                sprintf(
438
                    'Is attribute group? "%s" in %s',
439
                    $attribute->getName(),
440
                    json_encode($groupNames)
441
                )
442
            );
443
            /**
444
             * Is there a group name match?
445
             * Match the attribute name to the specified name in the plugin settings
446
             */
447
            if (in_array($attribute->getName(), $groupNames)) {
448
                /**
449
                 * Loop thru all of the attributes values because they could have multiple values.
450
                 * Example XML:
451
                 * <saml2:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
452
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
453
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
454
                 *           craft_admin
455
                 *           </saml2:AttributeValue>
456
                 *   <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
457
                 *           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
458
                 *           craft_member
459
                 *           </saml2:AttributeValue>
460
                 * </saml2:Attribute>
461
                 */
462
                foreach ($attribute->getAllAttributeValues() as $value) {
463
                    if ($group = $this->findOrCreateUserGroup($value)) {
464
                        Saml::debug(
465
                            sprintf(
466
                                'Assigning group: %s',
467
                                $group->name
468
                            )
469
                        );
470
                        $groups[] = $group->id;
471
                    }
472
                }
473
            }
474
        }
475
        /**
476
         * just return if this is empty
477
         */
478
        if (empty($groups)) {
479
            return true;
480
        }
481
482
        return \Craft::$app->getUsers()->assignUserToGroups($user->id, $groups);
483
    }
484
485
    /**
486
     * @param SamlResponse $response
487
     * @param User $user
488
     * @return User
489
     */
490
    protected function transformToUser(\LightSaml\Model\Protocol\Response $response, User $user)
491
    {
492
        $assertion = $response->getFirstAssertion();
493
494
        $attributeMap = Saml::getInstance()->getSettings()->responseAttributeMap;
495
496
        /**
497
         * Loop thru attributes and set to the user
498
         */
499
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
500
            if (isset($attributeMap[$attribute->getName()])) {
501
                $craftProperty = $attributeMap[$attribute->getName()];
502
503
                if (is_scalar($craftProperty)) {
504
                    //check if it exists as a property first
505
                    if (property_exists($user, $craftProperty)) {
506
                        Saml::debug(
507
                            sprintf(
508
                                'Attribute %s is scalar and should set value "%s" to user->%s',
509
                                $attribute->getName(),
510
                                $attribute->getFirstAttributeValue(),
511
                                $craftProperty
512
                            )
513
                        );
514
                        $user->{$craftProperty} = $attribute->getFirstAttributeValue();
515
                    }
516
                } else {
517
                    if (is_callable($craftProperty)) {
518
                        Saml::debug(
519
                            sprintf(
520
                                'Attribute %s is handled with a callable.',
521
                                $attribute->getName()
522
                            )
523
                        );
524
                        call_user_func($craftProperty, $user, $attribute);
525
                    }
526
                }
527
            }
528
        }
529
530
        return $user;
531
    }
532
533
    /**
534
     * @param string $groupName
535
     * @return UserGroup
536
     * @throws UserException
537
     * @throws \craft\errors\WrongEditionException
538
     */
539
    protected function findOrCreateUserGroup($groupName): UserGroup
540
    {
541
542
        $groupHandle = StringHelper::camelCase($groupName);
543
544
        if (! $userGroup = \Craft::$app->getUserGroups()->getGroupByHandle($groupHandle)) {
545
            if (! \Craft::$app->getUserGroups()->saveGroup(
546
                $userGroup = new UserGroup(
547
                    [
548
                        'name'   => $groupName,
549
                        'handle' => $groupHandle,
550
                    ]
551
                )
552
            )
553
            ) {
554
                throw new UserException("Error saving new group {$groupHandle}");
555
            }
556
        }
557
558
        return $userGroup;
559
    }
560
561
    /**
562
     * @param ProviderIdentityRecord $identity
563
     * @return bool
564
     * @throws UserException
565
     * @throws \Throwable
566
     */
567
    protected function loginUser(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
568
    {
569
        if ($identity->getUser()->getStatus() !== User::STATUS_ACTIVE) {
570
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
571
                throw new UserException("Can't activate user.");
572
            }
573
        }
574
575
        if (\Craft::$app->getUser()->login(
576
            $identity->getUser(),
577
            /**
578
             * @todo read session duration from the response
579
             */
580
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
581
        )
582
        ) {
583
            $identity->lastLoginDate = new \DateTime();
0 ignored issues
show
Documentation introduced by
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...
584
        } else {
585
            throw new UserException("User login failed.");
586
        }
587
588
        return true;
589
    }
590
}
591