Completed
Push — master ( c513b3...394509 )
by
unknown
02:39 queued 01:02
created

src/services/Login.php (1 issue)

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