Test Failed
Push — master ( b9585a...faf188 )
by Julien
12:03
created

Identity::getIdentity()   D

Complexity

Conditions 19
Paths 18

Size

Total Lines 75
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 42
c 3
b 1
f 0
dl 0
loc 75
ccs 0
cts 36
cp 0
rs 4.5166
cc 19
nc 18
nop 1
crap 380

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the Zemit Framework.
4
 *
5
 * (c) Zemit Team <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE.txt
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Zemit;
12
13
use Lcobucci\JWT\Builder;
14
use Lcobucci\JWT\Parser;
15
use Lcobucci\JWT\Signer\Ecdsa\Sha512;
16
use Phalcon\Acl\Role;
17
use Phalcon\Db\Column;
18
use Phalcon\Validation\Validator\Confirmation;
19
use Zemit\Di\Injectable;
20
use Phalcon\Messages\Message;
21
use Phalcon\Validation\Validator\PresenceOf;
22
use Zemit\Models\Session;
23
use Zemit\Models\User;
24
25
/**
26
 * Class Identity
27
 * {@inheritDoc}
28
 *
29
 * @author Julien Turbide <[email protected]>
30
 * @copyright Zemit Team <[email protected]>
31
 *
32
 * @since 1.0
33
 * @version 1.0
34
 *
35
 * @package Zemit
36
 */
37
class Identity extends Injectable
38
{
39
    /**
40
     * Without encryption
41
     */
42
    const MODE_DEFAULT = self::MODE_JWT;
43
    
44
    /**
45
     * Without encryption (raw string into the session)
46
     */
47
    const MODE_STRING = 'string';
48
    
49
    /**
50
     * Store using JWT (jwt encrypted into the session)
51
     */
52
    const MODE_JWT = 'jwt';
53
    
54
    /**
55
     * Locale mode for the prepare fonction
56
     * @var string
57
     */
58
    public string $mode = self::MODE_DEFAULT;
59
    
60
    /**
61
     * @var mixed|string|null
62
     */
63
    public $sessionKey = 'zemit-identity';
64
    
65
    /**
66
     * @var array
67
     */
68
    public $options = [];
69
    
70
    /**
71
     * @var User
72
     */
73
    public $user;
74
    
75
    /**
76
     * @var User
77
     */
78
    public $userAs;
79
    
80
    /**
81
     * @var Session
82
     */
83
    public $currentSession;
84
    
85
    /**
86
     * @var string|int|bool|null
87
     */
88
    public $identity;
89
    
90
    public function __construct($options = [])
91
    {
92
        $this->setOptions($options);
93
        $this->sessionKey = $this->getOption('sessionKey', $this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type string; however, parameter $default of Zemit\Identity::getOption() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
        $this->sessionKey = $this->getOption('sessionKey', /** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
94
        $this->setMode($this->getOption('mode', $this->mode));
95
//        $this->set($this->getFromSession());
96
    }
97
    
98
    /**
99
     * Set default options
100
     *
101
     * @param array $options
102
     */
103
    public function setOptions($options = [])
104
    {
105
        $this->options = $options;
106
    }
107
    
108
    /**
109
     * Getting an option value from the key, allowing to specify a default value
110
     *
111
     * @param $key
112
     * @param null $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
113
     *
114
     * @return mixed|null
115
     */
116
    public function getOption($key, $default = null)
117
    {
118
        return $this->options[$key] ?? $default;
119
    }
120
    
121
    /**
122
     * @return string
123
     */
124
    public function getSessionClass()
125
    {
126
        return $this->getOption('sessionClass') ?? \Zemit\Models\Session::class;
127
    }
128
    
129
    /**
130
     * @return string
131
     */
132
    public function getUserClass()
133
    {
134
        return $this->getOption('userClass') ?? \Zemit\Models\User::class;
135
    }
136
    
137
    /**
138
     * @return string
139
     */
140
    public function getGroupClass()
141
    {
142
        return $this->getOption('groupClass') ?? \Zemit\Models\Group::class;
143
    }
144
    
145
    /**
146
     * @return string
147
     */
148
    public function getRoleClass()
149
    {
150
        return $this->getOption('roleClass') ?? \Zemit\Models\Role::class;
151
    }
152
    
153
    /**
154
     * @return string
155
     */
156
    public function getTypeClass()
157
    {
158
        return $this->getOption('roleClass') ?? \Zemit\Models\Type::class;
159
    }
160
    
161
    /**
162
     * @return string
163
     */
164
    public function getEmailClass()
165
    {
166
        return $this->getOption('emailClass') ?? \Zemit\Models\Email::class;
167
    }
168
    
169
    /**
170
     * Get the current mode
171
     * @return string
172
     */
173
    public function getMode()
174
    {
175
        return $this->mode;
176
    }
177
    
178
    /**
179
     * Set the mode
180
     *
181
     * @param string $mode
182
     *
183
     * @throws \Exception Throw an exception if the mode is not supported
184
     */
185
    public function setMode($mode)
186
    {
187
        switch($mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
188
            case self::MODE_STRING:
189
            case self::MODE_JWT:
190
                $this->mode = $mode;
191
                break;
192
            default:
193
                throw new \Exception('Identity mode `' . $mode . '` is not supported.');
194
                break;
195
        }
196
    }
197
    
198
    /**
199
     * @return bool|mixed
200
     */
201
    public function getFromSession()
202
    {
203
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
0 ignored issues
show
Unused Code introduced by
The assignment to $ret is dead and can be removed.
Loading history...
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

203
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey) : null;
Loading history...
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

203
        $ret = $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
Loading history...
204
        
205
        if ($ret) {
206
            switch($this->mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
207
                case self::MODE_DEFAULT:
208
                    break;
209
                case self::MODE_JWT:
210
                    $ret = $this->jwt->parseToken($ret)->getClaim('identity');
0 ignored issues
show
Bug introduced by
The method parseToken() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
                    $ret = $this->jwt->/** @scrutinizer ignore-call */ parseToken($ret)->getClaim('identity');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property jwt does not exist on Zemit\Identity. Since you implemented __get, consider adding a @property annotation.
Loading history...
211
                    break;
212
            }
213
        }
214
        
215
        return json_decode($ret);
216
    }
217
    
218
    /**
219
     * Save an identity into the session
220
     *
221
     * @param int|string|null $identity
222
     */
223
    public function setIntoSession($identity)
224
    {
225
        
226
        $identity = json_encode($identity);
227
        
228
        $token = null;
229
        switch($this->mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
230
            case self::MODE_JWT:
231
                $token = $this->jwt->getToken(['identity' => $identity]);
0 ignored issues
show
Bug Best Practice introduced by
The property jwt does not exist on Zemit\Identity. Since you implemented __get, consider adding a @property annotation.
Loading history...
232
                break;
233
        }
234
        
235
        $this->session->set($this->sessionKey, $token ? : $identity);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
        $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $token ? : $identity);
Loading history...
236
    }
237
    
238
    /**
239
     * Set an identity
240
     *
241
     * @param int|string|null $identity
242
     */
243
    public function set($identity)
244
    {
245
        $this->setIntoSession($identity);
246
        $this->identity = $identity;
247
    }
248
    
249
    /**
250
     * Get the current identity
251
     * @return int|string|null
252
     */
253
    public function get()
254
    {
255
        $this->identity ??= $this->getFromSession();
256
        
257
        return $this->identity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->identity also could return the type boolean which is incompatible with the documented return type integer|null|string.
Loading history...
258
    }
259
    
260
    /**
261
     * @param array|null $roles
262
     * @param bool $or
263
     * @param bool $inherit
264
     *
265
     * @return bool
266
     */
267
    public function hasRole(?array $roles = null, bool $or = false, bool $inherit = true)
268
    {
269
        return $this->has($roles, array_keys($this->getRoleList($inherit) ? : []), $or);
270
    }
271
    
272
    /**
273
     * Return the current user ID
274
     *
275
     * @return string|int|bool
276
     */
277
    public function getUserId($as = false)
278
    {
279
        /** @var User $user */
280
        $user = $this->getUser($as);
281
        
282
        return $user ? $user->getId() : false;
0 ignored issues
show
introduced by
$user is of type Zemit\Models\User, thus it always evaluated to true.
Loading history...
283
    }
284
    
285
    /**
286
     * Check if the needles meet the haystack using nested arrays
287
     * Reversing ANDs and ORs within each nested subarray
288
     *
289
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), true); // 'dev' OR 'admin'
290
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), false); // 'dev' ADN 'admin'
291
     *
292
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles()); // 'dev' AND 'admin'
293
     * $this->has([['dev', 'admin']], $this->getUser()->getRoles()); // 'dev' OR 'admin'
294
     * $this->has([[['dev', 'admin']]], $this->getUser()->getRoles()); // 'dev' AND 'admin'
295
     *
296
     * @param array|string|null $needles Needles to match and meet the rules
297
     * @param array $haystack Haystack array to search into
298
     * @param bool $or True to force with "OR" , false to force "AND" condition
299
     *
300
     * @return bool Return true or false if the needles rules are being met
301
     */
302
    public function has($needles = null, array $haystack = [], $or = false)
303
    {
304
        if (!is_array($needles)) {
305
            $needles = [$needles];
306
        }
307
        
308
        $result = [];
309
        foreach ([...$needles] as $needle) {
310
            if (is_array($needle)) {
311
                $result [] = $this->has($needle, $haystack, !$or);
312
            }
313
            else {
314
                $result [] = in_array($needle, $haystack, true);
315
            }
316
        }
317
        
318
        return $or ?
319
            !in_array(false, $result, true) :
320
            in_array(true, $result, true)
321
        ;
322
    }
323
    
324
    /**
325
     * Create a refresh a session
326
     *
327
     * @param bool $refresh
328
     *
329
     * @throws \Phalcon\Security\Exception
330
     */
331
    public function getJwt($refresh = false)
332
    {
333
        [$key, $token] = $this->getKeyToken();
334
        
335
        $key ??= $this->security->getRandom()->uuid();
336
        $token ??= $this->security->getRandom()->hex(512);
337
        
338
        $sessionClass = $this->getSessionClass();
339
        $session = $this->getSession($key, $token) ? : new $sessionClass();
340
        
341
        if ($session && $refresh) {
342
            $token = $this->security->getRandom()->hex(512);
343
        }
344
        
345
        $session->setKey($key);
346
        $session->setToken($session->hash($key . $token));
347
        $session->setDate(date('Y-m-d H:i:s'));
348
        $store = ['key' => $session->getKey(), 'token' => $token];
349
        
350
        ($save = $session->save()) ?
351
            $this->session->set($this->sessionKey, $store) :
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

351
            $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $store) :
Loading history...
352
            $this->session->remove($this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::remove() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

352
            $this->session->remove(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
353
        
354
        return [
355
            'saved' => $save,
356
            'stored' => $this->session->has($this->sessionKey),
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

356
            'stored' => $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey),
Loading history...
357
            'refreshed' => $save && $refresh,
358
            'validated' => $session->checkHash($session->getToken(), $session->getKey() . $token),
359
            'messages' => $session ? $session->getMessages() : [],
360
            'jwt' => $this->getJwtToken($this->sessionKey, $store),
361
        ];
362
    }
363
    
364
    /**
365
     * Get basic Identity information
366
     *
367
     * @return array
368
     */
369
    public function getIdentity($inherit = true)
370
    {
371
        $user = $this->getUser();
372
        $userAs = $this->getUserAs();
373
        
374
        $roleList = [];
375
        $groupList = [];
376
        $typeList = [];
377
        
378
        if ($user) {
379
            if ($user->rolelist) {
0 ignored issues
show
Bug introduced by
The property rolelist does not exist on true.
Loading history...
380
                foreach ($user->rolelist as $role) {
381
                    $roleList [$role->getIndex()] = $role;
382
                }
383
            }
384
            
385
            if ($user->grouplist) {
0 ignored issues
show
Bug introduced by
The property grouplist does not exist on true.
Loading history...
386
                foreach ($user->grouplist as $group) {
387
                    $groupList [$group->getIndex()] = $group;
388
                    
389
                    if ($group->rolelist) {
390
                        foreach ($group->rolelist as $role) {
391
                            $roleList [$role->getIndex()] = $role;
392
                        }
393
                    }
394
                }
395
            }
396
            
397
            if ($user->typelist) {
0 ignored issues
show
Bug introduced by
The property typelist does not exist on true.
Loading history...
398
                foreach ($user->typelist as $type) {
399
                    $typeList [$type->getIndex()] = $type;
400
                    
401
                    if ($type->grouplist) {
402
                        foreach ($type->grouplist as $group) {
403
                            $groupList [$group->getIndex()] = $group;
404
                            
405
                            if ($group->rolelist) {
406
                                foreach ($group->rolelist as $role) {
407
                                    $roleList [$role->getIndex()] = $role;
408
                                }
409
                            }
410
                        }
411
                    }
412
                }
413
            }
414
        }
415
        
416
        // Append inherit roles
417
        if ($inherit) {
418
            foreach ($roleList as $role) {
419
                $roleIndex = $role->getIndex();
420
                $configRoles = $this->config->path('permissions.roles.' . $roleIndex . '.inherit', false);
421
                if ($configRoles) {
422
                    foreach ($configRoles->toArray() as $configRole) {
423
                        if (!isset($roleList[$configRole])) {
424
                            $roleClass = $this->getRoleClass();
425
                            
426
                            /** @var \Zemit\Models\Role $inheritedRole */
427
                            $inheritedRole = $roleClass::findFirstByIndex($configRole);
428
                            $roleList[$configRole] = $inheritedRole;
429
                        }
430
                    }
431
                }
432
            }
433
        }
434
        
435
        // We don't need userAs group / type / role list
436
        return [
437
            'loggedIn' => $this->isLoggedIn(),
438
            'loggedInAs' => $this->isLoggedInAs(),
439
            'user' => $user,
440
            'userAs' => $userAs,
441
            'roleList' => $roleList,
442
            'typeList' => $typeList,
443
            'groupList' => $groupList,
444
        ];
445
    }
446
    
447
    /**
448
     * Return true if the user is currently logged in
449
     *
450
     * @param bool $as
451
     * @param bool $refresh
452
     *
453
     * @return bool
454
     */
455
    public function isLoggedIn($as = false, $refresh = false)
456
    {
457
        return !!$this->getUser($as, $refresh);
458
    }
459
    
460
    /**
461
     * Return true if the user is currently logged in
462
     *
463
     * @param bool $refresh
464
     *
465
     * @return bool
466
     */
467
    public function isLoggedInAs($refresh = false)
468
    {
469
        return $this->isLoggedIn(true, $refresh);
470
    }
471
    
472
    /**
473
     * Get the User related to the current session
474
     *
475
     * @return User|bool
476
     */
477
    public function getUser($as = false, $refresh = false)
478
    {
479
        $property = $as ? 'userAs' : 'user';
480
        
481
        if ($refresh) {
482
            $this->$property = null;
483
        }
484
        
485
        if (is_null($this->$property)) {
486
            
487
            $session = $this->getSession();
488
            
489
            $userClass = $this->getUserClass();
490
            $user = !$session ? false : $userClass::findFirstWithById([
491
                'RoleList',
492
                'GroupList.RoleList',
493
                'TypeList.GroupList.RoleList',
494
//            'GroupList.TypeList.RoleList', // @TODO do it
495
//            'TypeList.RoleList', // @TODO do it
496
            ], $as ? $session->getAsUserId() : $session->getUserId());
497
            
498
            $this->$property = $user ? $user : false;
499
        }
500
        
501
        return $this->$property;
502
    }
503
    
504
    /**
505
     * Get the User As related to the current session
506
     *
507
     * @return bool|User
508
     */
509
    public function getUserAs()
510
    {
511
        return $this->getUser(true);
512
    }
513
    
514
    /**
515
     * Get the "Roles" related to the current session
516
     * @return array
517
     */
518
    public function getRoleList($inherit = true)
519
    {
520
        return $this->getIdentity($inherit)['roleList'] ?? [];
521
    }
522
    
523
    /**
524
     * Get the "Groups" related to the current session
525
     * @return array
526
     */
527
    public function getGroupList()
528
    {
529
        return $this->getIdentity()['groupList'] ?? [];
530
    }
531
    
532
    /**
533
     * Get the "Types" related to the current session
534
     * @return array
535
     */
536
    public function getTypeList()
537
    {
538
        return $this->getIdentity()['typeList'] ?? [];
539
    }
540
    
541
    /**
542
     * @param array|null $roles
543
     *
544
     * @return array
545
     */
546
    public function getAclRoles(array $roleList = null)
547
    {
548
        $roleList ??= $this->getRoleList();
549
        
550
        $aclRoles = [];
551
        $aclRoles['everyone'] = new Role('everyone');
552
        foreach ($roleList as $role) {
553
            $aclRoles[$role->getIndex()] ??= new Role($role->getIndex());
554
        }
555
        
556
        return array_values($aclRoles);
557
    }
558
    
559
    /**
560
     * @param $userId
561
     *
562
     * @return array
563
     */
564
    public function loginAs($params)
565
    {
566
        /** @var Session $session */
567
        $session = $this->getSession();
568
        
569
        $validation = new Validation();
570
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
571
        $validation->validate($params);
572
        
573
        $userId = $session->getUserId();
574
        
575
        if (!empty($userId) && !empty($params['userId'])) {
576
            
577
            if ((int)$params['userId'] === (int)$userId) {
578
                return $this->logoutAs();
579
            }
580
            
581
            $userClass = $this->getUserClass();
582
            $asUser = $userClass::findFirstById((int)$params['userId']);
583
            
584
            if ($asUser) {
585
                if ($this->hasRole(['admin', 'dev'])) {
586
                    $session->setAsUserId($userId);
587
                    $session->setUserId($params['userId']);
588
                }
589
            }
590
            else {
591
                $validation->appendMessage(new Message('User Not Found', 'userId', 'PresenceOf', 404));
592
            }
593
        }
594
        
595
        $saved = $session ? $session->save() : false;
0 ignored issues
show
introduced by
$session is of type Zemit\Models\Session, thus it always evaluated to true.
Loading history...
596
        foreach ($session->getMessages() as $message) {
597
            $validation->appendMessage($message);
598
        }
599
        
600
        return [
601
            'saved' => $saved,
602
            'loggedIn' => $this->isLoggedIn(false, true),
603
            'loggedInAs' => $this->isLoggedIn(true, true),
604
            'messages' => $validation->getMessages(),
605
        ];
606
    }
607
    
608
    /**
609
     * @return array
610
     */
611
    public function logoutAs()
612
    {
613
        /** @var Session $session */
614
        $session = $this->getSession();
615
        
616
        $asUserId = $session->getAsUserId();
617
        $userId = $session->getUserId();
618
        if (!empty($asUserId) && !empty($userId)) {
619
            $session->setUserId($asUserId);
620
            $session->setAsUserId(null);
621
        }
622
        
623
        return [
624
            'saved' => $session ? $session->save() : false,
0 ignored issues
show
introduced by
$session is of type Zemit\Models\Session, thus it always evaluated to true.
Loading history...
625
            'loggedIn' => $this->isLoggedIn(false, true),
626
            'loggedInAs' => $this->isLoggedIn(true, true),
627
            'messages' => $session->getMessages(),
628
        ];
629
    }
630
    
631
    /**
632
     *
633
     */
634
    public function oauth2(string $provider, int $id, string $accessToken, ?array $meta = [])
635
    {
636
        $loggedInUser = null;
637
        $saved = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $saved is dead and can be removed.
Loading history...
638
        
639
        // retrieve and prepare oauth2 entity
640
        $oauth2 = Oauth2::findFirst([
0 ignored issues
show
Bug introduced by
The type Zemit\Oauth2 was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
641
            'provider = :provider: and id = :id:',
642
            'bind' => [
643
                'provider' => $this->filter->sanitize($provider, 'string'),
644
                'id' => (int)$id,
645
            ],
646
            'bindTypes' => [
647
                'provider' => Column::BIND_PARAM_STR,
648
                'id' => Column::BIND_PARAM_INT,
649
            ],
650
        ]);
651
        if (!$oauth2) {
652
            $oauth2 = new Oauth2();
653
            $oauth2->setProviderName($provider);
654
            $oauth2->setProviderId($id);
655
        }
656
        $oauth2->setAccessToken($accessToken);
657
        $oauth2->setMeta($meta);
658
        $oauth2->setName($meta['name'] ?? null);
659
        $oauth2->setFirstName($meta['first_name'] ?? null);
660
        $oauth2->setLastName($meta['last_name'] ?? null);
661
        $oauth2->setEmail($meta['email'] ?? null);
662
        
663
        // ge the current session
664
        $session = $this->getSession();
665
        
666
        // link the current user to the oauth2 entity
667
        $oauth2UserId = $oauth2->getUserId();
668
        $sessionUserId = $session->getUserId();
669
        if (empty($oauth2UserId) && !empty($sessionUserId)) {
670
            $oauth2->setUserId($sessionUserId);
671
        }
672
        
673
        // prepare validation
674
        $validation = new Validation();
675
        
676
        // save the oauth2 entity
677
        $saved = $oauth2->save();
678
        
679
        // append oauth2 error messages
680
        foreach ($oauth2->getMessages() as $message) {
681
            $validation->appendMessage($message);
682
        }
683
        
684
        // a session is required
685
        if (!$session) {
686
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
687
        }
688
        
689
        // user id is required
690
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
691
        $validation->validate($oauth2 ? $oauth2->toArray() : []);
692
        
693
        // All validation passed
694
        if ($saved && !$validation->getMessages()->count()) {
695
    
696
            $user = $this->findUser($oauth2->getUserId());
697
    
698
            // user not found, login failed
699
            if (!$user) {
700
                $validation->appendMessage(new Message('Login Failed', ['id'], 'LoginFailed', 401));
701
            }
702
            
703
            // access forbidden, login failed
704
            else if ($user->isDeleted()) {
705
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
706
            }
707
            
708
            // login success
709
            else {
710
                $loggedInUser = $user;
711
            }
712
            
713
            // Set the oauth user id into the session
714
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
715
            $saved = $session->save();
716
            
717
            // append session error messages
718
            foreach ($session->getMessages() as $message) {
719
                $validation->appendMessage($message);
720
            }
721
        }
722
        
723
        return [
724
            'saved' => $saved,
725
            'loggedIn' => $this->isLoggedIn(false, true),
726
            'loggedInAs' => $this->isLoggedIn(true, true),
727
            'messages' => $validation->getMessages(),
728
        ];
729
    }
730
    
731
    /**
732
     * Login Action
733
     * - Require an active session to bind the logged in userId
734
     *
735
     * @return array
736
     */
737
    public function login(array $params = null)
738
    {
739
        $loggedInUser = null;
740
        $saved = null;
741
        
742
        $session = $this->getSession();
743
        
744
        $validation = new Validation();
745
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
746
        $validation->add('password', new PresenceOf(['message' => 'password is required']));
747
        $validation->validate($params);
748
        
749
        if (!$session) {
750
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
751
        }
752
        
753
        $messages = $validation->getMessages();
754
        
755
        if (!$messages->count()) {
756
            $user = $this->findUser($params['email'] ?? $params['username']);
757
            
758
            if (!$user) {
759
                // user not found, login failed
760
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
761
            }
762
            
763
            else if (empty($user->getPassword())) {
764
                // password disabled, login failed
765
                $validation->appendMessage(new Message('Password Login Disabled', 'password', 'LoginFailed', 401));
766
            }
767
            
768
            else if (!$user->checkPassword($params['password'])) {
769
                // password failed, login failed
770
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
771
            }
772
            
773
            else if ($user->isDeleted()) {
774
                // access forbidden, login failed
775
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
776
            }
777
            
778
            // login success
779
            else {
780
                $loggedInUser = $user;
781
            }
782
            
783
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
784
            $saved = $session->save();
785
            
786
            foreach ($session->getMessages() as $message) {
787
                $validation->appendMessage($message);
788
            }
789
        }
790
        
791
        return [
792
            'saved' => $saved,
793
            'loggedIn' => $this->isLoggedIn(false, true),
794
            'loggedInAs' => $this->isLoggedIn(true, true),
795
            'messages' => $validation->getMessages(),
796
        ];
797
    }
798
    
799
    /**
800
     * Log the user out from the database session
801
     *
802
     * @return bool|mixed|null
803
     */
804
    public function logout()
805
    {
806
        $saved = false;
807
        
808
        $session = $this->getSession();
809
        $validation = new Validation();
810
        $validation->validate();
811
        
812
        if (!$session) {
813
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
814
        }
815
        else {
816
            // Logout
817
            $session->setUserId(null);
818
            $session->setAsUserId(null);
819
            $saved = $session->save();
820
            
821
            foreach ($session->getMessages() as $message) {
822
                $validation->appendMessage($message);
823
            }
824
        }
825
        
826
        return [
827
            'saved' => $saved,
828
            'loggedIn' => $this->isLoggedIn(false, true),
829
            'loggedInAs' => $this->isLoggedIn(true, true),
830
            'messages' => $validation->getMessages(),
831
        ];
832
    }
833
    
834
    /**
835
     * @param array|null $params
836
     *
837
     * @return array
838
     */
839
    public function reset(array $params = null)
840
    {
841
        $saved = false;
842
        $sent = false;
843
        $token = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $token is dead and can be removed.
Loading history...
844
        
845
        $session = $this->getSession();
846
        $validation = new Validation();
847
        
848
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
849
        $validation->validate($params);
850
        
851
        if (!$session) {
852
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
853
        }
854
        else {
855
            $user = false;
856
            if (isset($params['email'])) {
857
                $userClass = $this->getUserClass();
858
                $user = $userClass::findFirstByEmail($params['email']);
859
            }
860
            
861
            // Reset
862
            if ($user) {
863
                
864
                // Password reset request
865
                if (empty($params['token'])) {
866
                    
867
                    // Generate a new token
868
                    $token = $user->prepareToken();
869
                    
870
                    // Send it by email
871
                    $emailClass = $this->getEmailClass();
872
                    
873
                    $email = new $emailClass();
874
                    $email->setTemplateByIndex('reset-password');
875
                    $email->setTo($user->getEmail());
876
                    $meta = [];
877
                    $meta['user'] = $user->expose(['User' => [
878
                        false,
879
                        'firstName',
880
                        'lastName',
881
                        'email',
882
                    ]]);
883
                    $meta['resetLink'] = $this->url->get('/reset-password/' . $token);
884
                    
885
                    $email->setMeta($meta);
886
                    $saved = $user->save();
887
                    $sent = $saved ? $email->send() : false;
888
                    
889
                    // Appending error messages
890
                    foreach (['user', 'email'] as $e) {
891
                        foreach ($$e->getMessages() as $message) {
892
                            $validation->appendMessage($message);
893
                        }
894
                    }
895
                }
896
                
897
                // Password reset
898
                else {
899
                    $validation->add('password', new PresenceOf(['message' => 'password is required']));
900
                    $validation->add('passwordConfirm', new PresenceOf(['message' => 'password confirm is required']));
901
                    $validation->add('password', new Confirmation(['message' => 'password does not match passwordConfirm', 'with' => 'passwordConfirm']));
902
                    $validation->validate($params);
903
                    
904
                    if (!$user->checkToken($params['token'])) {
905
                        $validation->appendMessage(new Message('invalid token', 'token', 'NotValid', 400));
906
                    }
907
                    else {
908
                        if (!count($validation->getMessages())) {
909
                            $params['token'] = null;
910
                            $user->assign($params, ['token', 'password', 'passwordConfirm']);
911
                            $saved = $user->save();
912
                        }
913
                    }
914
                }
915
                
916
                // Appending error messages
917
                foreach ($user->getMessages() as $message) {
918
                    $validation->appendMessage($message);
919
                }
920
            }
921
            else {
922
                $validation->appendMessage(new Message('User not found', 'user', 'PresenceOf', 404));
923
            }
924
        }
925
        
926
        return [
927
            'saved' => $saved,
928
            'sent' => $sent,
929
            'messages' => $validation->getMessages(),
930
        ];
931
    }
932
    
933
    /**
934
     * Get key / token fields to use for the session fetch & validation
935
     *
936
     * @return array
937
     */
938
    public function getKeyToken()
939
    {
940
        $basicAuth = $this->request->getBasicAuth();
941
        $authorization = array_filter(explode(' ', $this->request->getHeader('Authorization') ? : ''));
942
        
943
        $jwt = $this->request->get('jwt', 'string');
944
        $key = $this->request->get('key', 'string');
945
        $token = $this->request->get('token', 'string');
946
        
947
        if (!empty($jwt)) {
948
            $sessionClaim = $this->getClaim($jwt, $this->sessionKey);
949
            $key = $sessionClaim->key ?? null;
950
            $token = $sessionClaim->token ?? null;
951
        }
952
        
953
        else if (!empty($basicAuth)) {
954
            $key = $basicAuth['username'] ?? null;
955
            $token = $basicAuth['password'] ?? null;
956
        }
957
        
958
        else if (!empty($authorization)) {
959
            $authorizationType = $authorization[0] ?? 'Bearer';
960
            $authorizationToken = $authorization[1] ?? null;
961
            
962
            if (strtolower($authorizationType) === 'bearer') {
963
                $sessionClaim = $this->getClaim($authorizationToken, $this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $authorizationToken can also be of type null; however, parameter $token of Zemit\Identity::getClaim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

963
                $sessionClaim = $this->getClaim(/** @scrutinizer ignore-type */ $authorizationToken, $this->sessionKey);
Loading history...
964
                $key = $sessionClaim->key ?? null;
965
                $token = $sessionClaim->token ?? null;
966
            }
967
        }
968
        
969
        else if ($this->session->has($this->sessionKey)) {
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

969
        else if ($this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey)) {
Loading history...
970
            $sessionStore = $this->session->get($this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

970
            $sessionStore = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
971
            $key = $sessionStore['key'] ?? null;
972
            $token = $sessionStore['token'] ?? null;
973
        }
974
        
975
        return [$key, $token];
976
    }
977
    
978
    /**
979
     * Return the session by key if the token is valid
980
     *
981
     * @param string $key
982
     * @param string $token
983
     * @param bool $refresh Pass true to force a session fetch from the database
984
     *
985
     * @return void|bool|Session Return the session by key if the token is valid, false otherwise
986
     */
987
    public function getSession(string $key = null, string $token = null, bool $refresh = false)
988
    {
989
        if (!isset($key, $token)) {
990
            [$key, $token] = $this->getKeyToken();
991
        }
992
        
993
        if (empty($key) || empty($token)) {
994
            return false;
995
        }
996
        
997
        if ($refresh) {
998
            $this->currentSession = null;
999
        }
1000
        
1001
        if (isset($this->currentSession)) {
1002
            return $this->currentSession;
1003
        }
1004
        
1005
        $sessionClass = $this->getSessionClass();
1006
        $session = $sessionClass::findFirstByKey($this->filter->sanitize($key, 'string'));
1007
        
1008
        if ($session && $session->checkHash($session->getToken(), $key . $token)) {
1009
            $this->currentSession = $session;
1010
        }
1011
        
1012
        return $this->currentSession;
1013
    }
1014
    
1015
    /**
1016
     * Get a claim
1017
     * @TODO generate private & public keys
1018
     *
1019
     * @param string $token
1020
     * @param string|null $claim
1021
     *
1022
     * @return array|mixed
1023
     */
1024
    public function getClaim(string $token, string $claim = null)
1025
    {
1026
        $jwt = (new Parser())->parse((string)$token);
1027
        
1028
        return $claim ? $jwt->getClaim($claim) : $jwt->getClaims();
1029
    }
1030
    
1031
    /**
1032
     * Generate a new JWT
1033
     * @TODO generate private & public keys and use them
1034
     *
1035
     * @param $claim
1036
     * @param $data
1037
     *
1038
     * @return string
1039
     */
1040
    public function getJwtToken($claim, $data)
1041
    {
1042
        $uri = $this->request->getScheme() . '://' . $this->request->getHttpHost();
1043
1044
//        $privateKey = new Key('file://{path to your private key}');
1045
        $signer = new Sha512();
1046
        $time = time();
1047
        
1048
        $token = (new Builder())
1049
            ->issuedBy($uri) // Configures the issuer (iss claim)
1050
            ->permittedFor($uri) // Configures the audience (aud claim)
1051
            ->identifiedBy($claim, true) // Configures the id (jti claim), replicating as a header item
1052
            ->issuedAt($time) // Configures the time that the token was issue (iat claim)
1053
            ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
1054
            ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
1055
            ->withClaim($claim, $data) // Configures a new claim, called "uid"
1056
            ->getToken($signer) // Retrieves the generated token
1057
//            ->getToken($signer,  $privateKey); // Retrieves the generated token
1058
        ;
1059
        
1060
        return (string)$token;
1061
    }
1062
    
1063
    /**
1064
     * Retrieve the user from a username or an email
1065
     *
1066
     * @param $usernameOrEmail
1067
     *
1068
     * @return mixed
1069
     * @todo maybe move this into user model?
1070
     *
1071
     */
1072
    public function findUser($idUsernameEmail)
1073
    {
1074
        if (empty($idUsernameEmail)) {
1075
            return false;
1076
        }
1077
        
1078
        $userClass = $this->getUserClass();
1079
        
1080
        if (!is_int($idUsernameEmail)) {
1081
            $usernameEmail = $this->filter->sanitize($idUsernameEmail, ['string', 'trim']);
1082
            $user = $userClass::findFirst([
1083
                'email = :email: or username = :username:',
1084
                'bind' => [
1085
                    'email' => $usernameEmail,
1086
                    'username' => $usernameEmail,
1087
                ],
1088
                'bindTypes' => [
1089
                    'email' => Column::BIND_PARAM_STR,
1090
                    'username' => Column::BIND_PARAM_STR,
1091
                ],
1092
            ]);
1093
        }
1094
        else {
1095
            $user = $userClass::findFirstById((int)$idUsernameEmail);
1096
        }
1097
        
1098
        return $user;
1099
    }
1100
}
1101