Test Failed
Push — master ( 3c398a...91321d )
by Julien
03:48
created

Identity::getIdentity()   D

Complexity

Conditions 18
Paths 27

Size

Total Lines 80
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 342

Importance

Changes 6
Bugs 3 Features 0
Metric Value
eloc 44
c 6
b 3
f 0
dl 0
loc 80
ccs 0
cts 38
cp 0
rs 4.8666
cc 18
nc 27
nop 1
crap 342

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|Mvc\Model|\Zemit\Models\Session
123
     */
124
    public function getSessionClass()
125
    {
126
        return $this->config->getModelClass(\Zemit\Models\Session::class);
127
    }
128
    
129
    /**
130
     * @return string|Mvc\Model|\Zemit\Models\User
131
     */
132
    public function getUserClass()
133
    {
134
        return $this->config->getModelClass(\Zemit\Models\User::class);
135
    }
136
    
137
    /**
138
     * @return string|Mvc\Model|\Zemit\Models\Group
139
     */
140
    public function getGroupClass()
141
    {
142
        return $this->config->getModelClass(\Zemit\Models\Group::class);
143
    }
144
    
145
    /**
146
     * @return string|Mvc\Model|\Zemit\Models\Role
147
     */
148
    public function getRoleClass()
149
    {
150
        return $this->config->getModelClass(\Zemit\Models\Role::class);
151
    }
152
    
153
    /**
154
     * @return string|Mvc\Model|\Zemit\Models\Type
155
     */
156
    public function getTypeClass()
157
    {
158
        return $this->config->getModelClass(\Zemit\Models\Type::class);
159
    }
160
    
161
    /**
162
     * @return string
163
     */
164
    public function getEmailClass()
165
    {
166
        return $this->config->getModelClass(\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) {
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) {
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) {
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' AND '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
     * Create a refresh a session
325
     *
326
     * @param bool $refresh
327
     *
328
     * @throws \Phalcon\Security\Exception
329
     */
330
    public function getJwt($refresh = false)
331
    {
332
        [$key, $token] = $this->getKeyToken();
333
        
334
        $key ??= $this->security->getRandom()->uuid();
335
        $token ??= $this->security->getRandom()->hex(512);
336
        $newToken = $refresh? $this->security->getRandom()->hex(512) : $token;
337
        $date = date('Y-m-d H:i:s');
338
        
339
        $sessionClass = $this->getSessionClass();
340
        $session = $this->getSession($key, $token) ?: new $sessionClass();
341
        $session->setKey($key);
342
        $session->setToken($session->hash($key . $newToken));
343
        $session->setDate($date);
344
        $store = ['key' => $session->getKey(), 'token' => $newToken];
345
        
346
        ($save = $session->save()) ?
347
            $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

347
            $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $store) :
Loading history...
348
            $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

348
            $this->session->remove(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
349
        
350
        return [
351
            'saved' => $save,
352
            '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

352
            'stored' => $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey),
Loading history...
353
            'refreshed' => $save && $refresh,
354
            'validated' => $session->checkHash($session->getToken(), $session->getKey() . $token),
355
            'messages' => $session->getMessages(),
356
            'jwt' => $this->getJwtToken($this->sessionKey, $store),
357
        ];
358
    }
359
    
360
    /**
361
     * Get basic Identity information
362
     *
363
     * @param bool $inherit
364
     *
365
     * @return array
366
     */
367
    public function getIdentity(bool $inherit = true)
368
    {
369
        $user = $this->getUser();
370
        $userAs = $this->getUserAs();
371
        
372
        $roleList = [];
373
        $groupList = [];
374
        $typeList = [];
375
        
376
        if ($user) {
377
            if ($user->rolelist) {
0 ignored issues
show
Bug Best Practice introduced by
The property rolelist does not exist on Zemit\Models\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
378
                foreach ($user->rolelist as $role) {
379
                    $roleList [$role->getIndex()] = $role;
380
                }
381
            }
382
            
383
            if ($user->grouplist) {
0 ignored issues
show
Bug Best Practice introduced by
The property grouplist does not exist on Zemit\Models\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
384
                foreach ($user->grouplist as $group) {
385
                    $groupList [$group->getIndex()] = $group;
386
                    
387
                    if ($group->rolelist) {
388
                        foreach ($group->rolelist as $role) {
389
                            $roleList [$role->getIndex()] = $role;
390
                        }
391
                    }
392
                }
393
            }
394
            
395
            if ($user->typelist) {
0 ignored issues
show
Bug Best Practice introduced by
The property typelist does not exist on Zemit\Models\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
396
                foreach ($user->typelist as $type) {
397
                    $typeList [$type->getIndex()] = $type;
398
                    
399
                    if ($type->grouplist) {
400
                        foreach ($type->grouplist as $group) {
401
                            $groupList [$group->getIndex()] = $group;
402
                            
403
                            if ($group->rolelist) {
404
                                foreach ($group->rolelist as $role) {
405
                                    $roleList [$role->getIndex()] = $role;
406
                                }
407
                            }
408
                        }
409
                    }
410
                }
411
            }
412
        }
413
        
414
        // Append inherit roles
415
        if ($inherit) {
416
            $roleIndexList = [];
417
            foreach ($roleList as $role) {
418
                $roleIndexList [] = $role->getIndex();
419
            }
420
            
421
            $inheritedRoleIndexList = $this->getInheritedRoleList($roleIndexList);
422
            if (!empty($inheritedRoleIndexList)) {
423
                
424
                /** @var \Phalcon\Mvc\Model\Resultset $inheritedRoleEntity */
425
                $inheritedRoleList = $this->getRoleClass()::find([
426
                    'index in ({role:array})', // @todo should filter soft-deleted roles?
427
                    'bind' => ['role' => $inheritedRoleIndexList],
428
                    'bindTypes' => ['role' => Column::BIND_PARAM_STR],
429
                ]);
430
                
431
                /** @var Models\Role $inheritedRoleEntity */
432
                foreach ($inheritedRoleList as $inheritedRoleEntity) {
433
                    $roleList[$inheritedRoleEntity->getIndex()] = $inheritedRoleEntity;
434
                }
435
            }
436
        }
437
        
438
        // We don't need userAs group / type / role list
439
        return [
440
            'loggedIn' => $this->isLoggedIn(),
441
            'loggedInAs' => $this->isLoggedInAs(),
442
            'user' => $user,
443
            'userAs' => $userAs,
444
            'roleList' => $roleList,
445
            'typeList' => $typeList,
446
            'groupList' => $groupList,
447
        ];
448
    }
449
    
450
    /**
451
     * Return the list of inherited role list (recursively)
452
     *
453
     * @param array $roleIndexList
454
     *
455
     * @return array List of inherited role list (recursive)
456
     */
457
    public function getInheritedRoleList(array $roleIndexList = [])
458
    {
459
        $inheritedRoleList = [];
460
        $processedRoleIndexList = [];
461
        
462
        // While we still have role index list to process
463
        while (!empty($roleIndexList)) {
464
            // Process role index list
465
            foreach ($roleIndexList as $roleIndex) {
466
                // Get inherited roles from config service
467
                $configRoleList = $this->config->path('permissions.roles.' . $roleIndex . '.inherit', false);
468
                
469
                if ($configRoleList) {
470
                    // Append inherited role to process list
471
                    $roleList = $configRoleList->toArray();
472
                    $roleIndexList = array_merge($roleIndexList, $roleList);
473
                    $inheritedRoleList = array_merge($inheritedRoleList, $roleList);
474
                }
475
                
476
                // Add role index to processed list
477
                $processedRoleIndexList [] = $roleIndex;
478
            }
479
            
480
            // Keep the unprocessed role index list
481
            $roleIndexList = array_filter(array_unique(array_diff($roleIndexList, $processedRoleIndexList)));
482
        }
483
        
484
        // Return the list of inherited role list (recursively)
485
        return array_values(array_filter(array_unique($inheritedRoleList)));
486
    }
487
    
488
    /**
489
     * Return true if the user is currently logged in
490
     *
491
     * @param bool $as
492
     * @param bool $refresh
493
     *
494
     * @return bool
495
     */
496
    public function isLoggedIn($as = false, $refresh = false)
497
    {
498
        return !!$this->getUser($as, $refresh);
499
    }
500
    
501
    /**
502
     * Return true if the user is currently logged in
503
     *
504
     * @param bool $refresh
505
     *
506
     * @return bool
507
     */
508
    public function isLoggedInAs($refresh = false)
509
    {
510
        return $this->isLoggedIn(true, $refresh);
511
    }
512
    
513
    /**
514
     * Get the User related to the current session
515
     *
516
     * @return User|bool
517
     */
518
    public function getUser($as = false, $refresh = false)
519
    {
520
        $property = $as ? 'userAs' : 'user';
521
        
522
        if ($refresh) {
523
            $this->$property = null;
524
        }
525
        
526
        if (is_null($this->$property)) {
527
            
528
            $session = $this->getSession();
529
            
530
            $userClass = $this->getUserClass();
531
            $user = !$session ? false : $userClass::findFirstWithById([
532
                'RoleList',
533
                'GroupList.RoleList',
534
                'TypeList.GroupList.RoleList',
535
//            'GroupList.TypeList.RoleList', // @TODO do it
536
//            'TypeList.RoleList', // @TODO do it
537
            ], $as ? $session->getAsUserId() : $session->getUserId());
538
            
539
            $this->$property = $user ? $user : false;
540
        }
541
        
542
        return $this->$property;
543
    }
544
    
545
    /**
546
     * Get the User As related to the current session
547
     *
548
     * @return bool|User
549
     */
550
    public function getUserAs()
551
    {
552
        return $this->getUser(true);
553
    }
554
    
555
    /**
556
     * Get the "Roles" related to the current session
557
     *
558
     * @param bool $inherit
559
     *
560
     * @return array|mixed
561
     */
562
    public function getRoleList(bool $inherit = true)
563
    {
564
        return $this->getIdentity($inherit)['roleList'] ?? [];
565
    }
566
    
567
    /**
568
     * Get the "Groups" related to the current session
569
     *
570
     * @param bool $inherit
571
     *
572
     * @return array
573
     */
574
    public function getGroupList(bool $inherit = true)
575
    {
576
        return $this->getIdentity($inherit)['groupList'] ?? [];
577
    }
578
    
579
    /**
580
     * Get the "Types" related to the current session
581
     *
582
     * @param bool $inherit
583
     *
584
     * @return array
585
     */
586
    public function getTypeList(bool $inherit = true)
587
    {
588
        return $this->getIdentity($inherit)['typeList'] ?? [];
589
    }
590
    
591
    /**
592
     * Return the list of ACL roles
593
     * - Reserved roles: guest, cli, everyone
594
     *
595
     * @param array|null $roleList
596
     * @return array
597
     */
598
    public function getAclRoles(?array $roleList = null): array
599
    {
600
        $roleList ??= $this->getRoleList();
601
        $aclRoles = [];
602
        
603
        // Add roles from databases
604
        foreach ($roleList as $role) {
605
            if ($role) {
606
                $aclRoles[$role->getIndex()] ??= new Role($role->getIndex());
607
            }
608
        }
609
        
610
        // Add guest role if no roles was detected
611
        if (count($aclRoles) === 0) {
612
            $aclRoles['guest'] = new Role('guest', 'Guest without role');
613
        }
614
        
615
        // Add console role
616
        if ($this->bootstrap->isConsole()) {
617
            $aclRoles['cli'] = new Role('cli', 'Console mode');
618
        }
619
        
620
        // Add everyone role
621
        $aclRoles['everyone'] = new Role('everyone', 'Everyone');
622
        
623
        return array_filter(array_values(array_unique($aclRoles)));
624
    }
625
    
626
    /**
627
     * @param $userId
628
     *
629
     * @return array
630
     */
631
    public function loginAs($params)
632
    {
633
        /** @var Session $session */
634
        $session = $this->getSession();
635
        
636
        $validation = new Validation();
637
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
638
        $validation->validate($params);
639
        
640
        $userId = $session->getUserId();
641
        
642
        if (!empty($userId) && !empty($params['userId'])) {
643
            
644
            if ((int)$params['userId'] === (int)$userId) {
645
                return $this->logoutAs();
646
            }
647
            
648
            $userClass = $this->getUserClass();
649
            $asUser = $userClass::findFirstById((int)$params['userId']);
650
            
651
            if ($asUser) {
652
                if ($this->hasRole(['admin', 'dev'])) {
653
                    $session->setAsUserId($userId);
654
                    $session->setUserId($params['userId']);
655
                }
656
            }
657
            else {
658
                $validation->appendMessage(new Message('User Not Found', 'userId', 'PresenceOf', 404));
659
            }
660
        }
661
        
662
        $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...
663
        foreach ($session->getMessages() as $message) {
664
            $validation->appendMessage($message);
665
        }
666
        
667
        return [
668
            'saved' => $saved,
669
            'loggedIn' => $this->isLoggedIn(false, true),
670
            'loggedInAs' => $this->isLoggedIn(true, true),
671
            'messages' => $validation->getMessages(),
672
        ];
673
    }
674
    
675
    /**
676
     * @return array
677
     */
678
    public function logoutAs()
679
    {
680
        /** @var Session $session */
681
        $session = $this->getSession();
682
        
683
        $asUserId = $session->getAsUserId();
684
        $userId = $session->getUserId();
685
        if (!empty($asUserId) && !empty($userId)) {
686
            $session->setUserId($asUserId);
687
            $session->setAsUserId(null);
688
        }
689
        
690
        return [
691
            '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...
692
            'loggedIn' => $this->isLoggedIn(false, true),
693
            'loggedInAs' => $this->isLoggedIn(true, true),
694
            'messages' => $session->getMessages(),
695
        ];
696
    }
697
    
698
    /**
699
     *
700
     */
701
    public function oauth2(string $provider, int $id, string $accessToken, ?array $meta = [])
702
    {
703
        $loggedInUser = null;
704
        $saved = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $saved is dead and can be removed.
Loading history...
705
        
706
        // retrieve and prepare oauth2 entity
707
        $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...
708
            'provider = :provider: and id = :id:',
709
            'bind' => [
710
                'provider' => $this->filter->sanitize($provider, 'string'),
711
                'id' => (int)$id,
712
            ],
713
            'bindTypes' => [
714
                'provider' => Column::BIND_PARAM_STR,
715
                'id' => Column::BIND_PARAM_INT,
716
            ],
717
        ]);
718
        if (!$oauth2) {
719
            $oauth2 = new Oauth2();
720
            $oauth2->setProviderName($provider);
721
            $oauth2->setProviderId($id);
722
        }
723
        $oauth2->setAccessToken($accessToken);
724
        $oauth2->setMeta($meta);
725
        $oauth2->setName($meta['name'] ?? null);
726
        $oauth2->setFirstName($meta['first_name'] ?? null);
727
        $oauth2->setLastName($meta['last_name'] ?? null);
728
        $oauth2->setEmail($meta['email'] ?? null);
729
        
730
        // get the current session
731
        $session = $this->getSession();
732
        
733
        // link the current user to the oauth2 entity
734
        $oauth2UserId = $oauth2->getUserId();
735
        $sessionUserId = $session->getUserId();
736
        if (empty($oauth2UserId) && !empty($sessionUserId)) {
737
            $oauth2->setUserId($sessionUserId);
738
        }
739
        
740
        // prepare validation
741
        $validation = new Validation();
742
        
743
        // save the oauth2 entity
744
        $saved = $oauth2->save();
745
        
746
        // append oauth2 error messages
747
        foreach ($oauth2->getMessages() as $message) {
748
            $validation->appendMessage($message);
749
        }
750
        
751
        // a session is required
752
        if (!$session) {
753
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
754
        }
755
        
756
        // user id is required
757
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
758
        $validation->validate($oauth2 ? $oauth2->toArray() : []);
759
        
760
        // All validation passed
761
        if ($saved && !$validation->getMessages()->count()) {
762
            
763
            $user = $this->findUser($oauth2->getUserId());
764
            
765
            // user not found, login failed
766
            if (!$user) {
767
                $validation->appendMessage(new Message('Login Failed', ['id'], 'LoginFailed', 401));
768
            }
769
            
770
            // access forbidden, login failed
771
            else if ($user->isDeleted()) {
0 ignored issues
show
Bug introduced by
The method isDeleted() does not exist on Phalcon\Mvc\Model\ResultInterface. It seems like you code against a sub-type of Phalcon\Mvc\Model\ResultInterface such as Phalcon\Mvc\Model. ( Ignorable by Annotation )

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

771
            else if ($user->/** @scrutinizer ignore-call */ isDeleted()) {
Loading history...
772
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
773
            }
774
            
775
            // login success
776
            else {
777
                $loggedInUser = $user;
778
            }
779
            
780
            // Set the oauth user id into the session
781
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
0 ignored issues
show
Bug introduced by
The method getId() does not exist on Phalcon\Mvc\Model\ResultInterface. It seems like you code against a sub-type of Phalcon\Mvc\Model\ResultInterface such as Phalcon\Mvc\Model. ( Ignorable by Annotation )

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

781
            $session->setUserId($loggedInUser ? $loggedInUser->/** @scrutinizer ignore-call */ getId() : null);
Loading history...
782
            $saved = $session->save();
783
            
784
            // append session error messages
785
            foreach ($session->getMessages() as $message) {
786
                $validation->appendMessage($message);
787
            }
788
        }
789
        
790
        return [
791
            'saved' => $saved,
792
            'loggedIn' => $this->isLoggedIn(false, true),
793
            'loggedInAs' => $this->isLoggedIn(true, true),
794
            'messages' => $validation->getMessages(),
795
        ];
796
    }
797
    
798
    /**
799
     * Login Action
800
     * - Require an active session to bind the logged in userId
801
     *
802
     * @return array
803
     */
804
    public function login(array $params = null)
805
    {
806
        $loggedInUser = null;
807
        $saved = null;
808
        
809
        $session = $this->getSession();
810
        
811
        $validation = new Validation();
812
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
813
        $validation->add('password', new PresenceOf(['message' => 'password is required']));
814
        $validation->validate($params);
815
        
816
        if (!$session) {
817
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
818
        }
819
        
820
        $messages = $validation->getMessages();
821
        
822
        if (!$messages->count()) {
823
            $user = $this->findUser($params['email'] ?? $params['username']);
824
            
825
            if (!$user) {
826
                // user not found, login failed
827
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
828
            }
829
            
830
            else if ($user->isDeleted()) {
831
                // access forbidden, login failed
832
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
833
            }
834
            
835
            else if (empty($user->getPassword())) {
0 ignored issues
show
Bug introduced by
The method getPassword() does not exist on Phalcon\Mvc\Model\ResultInterface. It seems like you code against a sub-type of Phalcon\Mvc\Model\ResultInterface such as Phalcon\Mvc\Model. ( Ignorable by Annotation )

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

835
            else if (empty($user->/** @scrutinizer ignore-call */ getPassword())) {
Loading history...
836
                // password disabled, login failed
837
                $validation->appendMessage(new Message('Password Login Disabled', 'password', 'LoginFailed', 401));
838
            }
839
            
840
            else if (!$user->checkPassword($params['password'])) {
0 ignored issues
show
Bug introduced by
The method checkPassword() does not exist on Phalcon\Mvc\Model\ResultInterface. It seems like you code against a sub-type of Phalcon\Mvc\Model\ResultInterface such as Phalcon\Mvc\Model. ( Ignorable by Annotation )

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

840
            else if (!$user->/** @scrutinizer ignore-call */ checkPassword($params['password'])) {
Loading history...
841
                // password failed, login failed
842
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
843
            }
844
            
845
            // login success
846
            else {
847
                $loggedInUser = $user;
848
            }
849
            
850
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
851
            $saved = $session->save();
852
            
853
            foreach ($session->getMessages() as $message) {
854
                $validation->appendMessage($message);
855
            }
856
        }
857
        
858
        return [
859
            'saved' => $saved,
860
            'loggedIn' => $this->isLoggedIn(false, true),
861
            'loggedInAs' => $this->isLoggedIn(true, true),
862
            'messages' => $validation->getMessages(),
863
        ];
864
    }
865
    
866
    /**
867
     * Log the user out from the database session
868
     *
869
     * @return bool|mixed|null
870
     */
871
    public function logout()
872
    {
873
        $saved = false;
874
        
875
        $session = $this->getSession();
876
        $validation = new Validation();
877
        $validation->validate();
878
        
879
        if (!$session) {
880
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
881
        }
882
        else {
883
            // Logout
884
            $session->setUserId(null);
885
            $session->setAsUserId(null);
886
            $saved = $session->save();
887
            
888
            foreach ($session->getMessages() as $message) {
889
                $validation->appendMessage($message);
890
            }
891
        }
892
        
893
        return [
894
            'saved' => $saved,
895
            'loggedIn' => $this->isLoggedIn(false, true),
896
            'loggedInAs' => $this->isLoggedIn(true, true),
897
            'messages' => $validation->getMessages(),
898
        ];
899
    }
900
    
901
    /**
902
     * @param array|null $params
903
     *
904
     * @return array
905
     */
906
    public function reset(array $params = null)
907
    {
908
        $saved = false;
909
        $sent = false;
910
        $token = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $token is dead and can be removed.
Loading history...
911
        
912
        $session = $this->getSession();
913
        $validation = new Validation();
914
        
915
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
916
        $validation->validate($params);
917
        
918
        if (!$session) {
919
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
920
        }
921
        else {
922
            $user = false;
923
            if (isset($params['email'])) {
924
                $userClass = $this->getUserClass();
925
                $user = $userClass::findFirstByEmail($params['email']);
926
            }
927
            
928
            // Reset
929
            if ($user) {
930
                
931
                // Password reset request
932
                if (empty($params['token'])) {
933
                    
934
                    // Generate a new token
935
                    $token = $user->prepareToken();
936
                    
937
                    // Send it by email
938
                    $emailClass = $this->getEmailClass();
939
                    
940
                    $email = new $emailClass();
941
                    $email->setViewPath('template/email');
942
                    $email->setTemplateByIndex('reset-password');
943
                    $email->setTo([$user->getEmail()]);
944
                    $meta = [];
945
                    $meta['user'] = $user->expose(['User' => [
946
                        false,
947
                        'firstName',
948
                        'lastName',
949
                        'email',
950
                    ]]);
951
                    $meta['resetLink'] = $this->url->get('/reset-password/' . $token);
952
                    
953
                    $email->setMeta($meta);
954
                    $saved = $user->save();
955
                    $sent = $saved ? $email->send() : false;
956
                    
957
                    // Appending error messages
958
                    foreach (['user', 'email'] as $e) {
959
                        foreach ($$e->getMessages() as $message) {
960
                            $validation->appendMessage($message);
961
                        }
962
                    }
963
                }
964
                
965
                // Password reset
966
                else {
967
                    $validation->add('password', new PresenceOf(['message' => 'password is required']));
968
                    $validation->add('passwordConfirm', new PresenceOf(['message' => 'password confirm is required']));
969
                    $validation->add('password', new Confirmation(['message' => 'password does not match passwordConfirm', 'with' => 'passwordConfirm']));
970
                    $validation->validate($params);
971
                    
972
                    if (!$user->checkToken($params['token'])) {
973
                        $validation->appendMessage(new Message('invalid token', 'token', 'NotValid', 400));
974
                    }
975
                    else {
976
                        if (!count($validation->getMessages())) {
977
                            $params['token'] = null;
978
                            $user->assign($params, ['token', 'password', 'passwordConfirm']);
979
                            $saved = $user->save();
980
                        }
981
                    }
982
                }
983
                
984
                // Appending error messages
985
                foreach ($user->getMessages() as $message) {
986
                    $validation->appendMessage($message);
987
                }
988
            }
989
            else {
990
//                $validation->appendMessage(new Message('User not found', 'user', 'PresenceOf', 404));
991
                // OWASP Protect User Enumeration
992
                $saved = true;
993
                $sent = true;
994
            }
995
        }
996
        
997
        return [
998
            'saved' => $saved,
999
            'sent' => $sent,
1000
            'messages' => $validation->getMessages(),
1001
        ];
1002
    }
1003
    
1004
    /**
1005
     * Get key / token fields to use for the session fetch & validation
1006
     *
1007
     * @return array
1008
     */
1009
    public function getKeyToken()
1010
    {
1011
        $basicAuth = $this->request->getBasicAuth();
1012
        $authorization = array_filter(explode(' ', $this->request->getHeader('Authorization') ?: ''));
1013
        
1014
        $jwt = $this->request->get('jwt', 'string');
1015
        $key = $this->request->get('key', 'string');
1016
        $token = $this->request->get('token', 'string');
1017
        
1018
        if (!empty($jwt)) {
1019
            $sessionClaim = $this->getClaim($jwt, $this->sessionKey);
1020
            $key = $sessionClaim->key ?? null;
1021
            $token = $sessionClaim->token ?? null;
1022
        }
1023
        
1024
        else if (!empty($basicAuth)) {
1025
            $key = $basicAuth['username'] ?? null;
1026
            $token = $basicAuth['password'] ?? null;
1027
        }
1028
        
1029
        else if (!empty($authorization)) {
1030
            $authorizationType = $authorization[0] ?? 'Bearer';
1031
            $authorizationToken = $authorization[1] ?? null;
1032
            
1033
            if (strtolower($authorizationType) === 'bearer') {
1034
                $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

1034
                $sessionClaim = $this->getClaim(/** @scrutinizer ignore-type */ $authorizationToken, $this->sessionKey);
Loading history...
1035
                $key = $sessionClaim->key ?? null;
1036
                $token = $sessionClaim->token ?? null;
1037
            }
1038
        }
1039
        
1040
        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

1040
        else if ($this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey)) {
Loading history...
1041
            $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

1041
            $sessionStore = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
1042
            $key = $sessionStore['key'] ?? null;
1043
            $token = $sessionStore['token'] ?? null;
1044
        }
1045
        
1046
        return [$key, $token];
1047
    }
1048
    
1049
    /**
1050
     * Return the session by key if the token is valid
1051
     *
1052
     * @param ?string $key
1053
     * @param ?string $token
1054
     * @param bool $refresh Pass true to force a session fetch from the database
1055
     *
1056
     * @return void|bool|Session Return the session by key if the token is valid, false otherwise
1057
     */
1058
    public function getSession(string $key = null, string $token = null, bool $refresh = false)
1059
    {
1060
        if (!isset($key, $token)) {
1061
            [$key, $token] = $this->getKeyToken();
1062
        }
1063
        
1064
        if (empty($key) || empty($token)) {
1065
            return false;
1066
        }
1067
        
1068
        if ($refresh) {
1069
            $this->currentSession = null;
1070
        }
1071
        
1072
        if (isset($this->currentSession)) {
1073
            return $this->currentSession;
1074
        }
1075
        
1076
        $sessionClass = $this->getSessionClass();
1077
        $session = $sessionClass::findFirstByKey($this->filter->sanitize($key, 'string'));
1078
        
1079
        if ($session && $session->checkHash($session->getToken(), $key . $token)) {
1080
            $this->currentSession = $session;
1081
        }
1082
        
1083
        return $this->currentSession;
1084
    }
1085
    
1086
    /**
1087
     * Get a claim
1088
     * @TODO generate private & public keys
1089
     *
1090
     * @param string $token
1091
     * @param string|null $claim
1092
     *
1093
     * @return array|mixed
1094
     */
1095
    public function getClaim(string $token, string $claim = null)
1096
    {
1097
        $jwt = (new Parser())->parse((string)$token);
1098
        
1099
        return $claim ? $jwt->getClaim($claim) : $jwt->getClaims();
1100
    }
1101
    
1102
    /**
1103
     * Generate a new JWT
1104
     * @TODO generate private & public keys and use them
1105
     *
1106
     * @param $claim
1107
     * @param $data
1108
     *
1109
     * @return string
1110
     */
1111
    public function getJwtToken($claim, $data) : string
1112
    {
1113
        $uri = $this->request->getScheme() . '://' . $this->request->getHttpHost();
1114
1115
//        $privateKey = new Key('file://{path to your private key}');
1116
        $signer = new Sha512();
1117
        $time = time();
1118
        
1119
        $token = (new Builder())
1120
            ->issuedBy($uri) // Configures the issuer (iss claim)
1121
            ->permittedFor($uri) // Configures the audience (aud claim)
1122
            ->identifiedBy($claim, true) // Configures the id (jti claim), replicating as a header item
1123
            ->issuedAt($time) // Configures the time that the token was issue (iat claim)
1124
            ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
1125
            ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
1126
            ->withClaim($claim, $data) // Configures a new claim, called "uid"
1127
            ->getToken($signer) // Retrieves the generated token
1128
//            ->getToken($signer,  $privateKey); // Retrieves the generated token
1129
        ;
1130
        
1131
        return (string)$token;
1132
    }
1133
    
1134
    /**
1135
     * Retrieve the user from a username or an email
1136
     * @param $idUsernameEmail
1137
     * @return false|\Phalcon\Mvc\Model\ResultInterface|\Phalcon\Mvc\ModelInterface|Models\Base\AbstractUser|null
1138
     * @todo maybe move this into user model?
1139
     *
1140
     */
1141
    public function findUser($idUsernameEmail)
1142
    {
1143
        if (empty($idUsernameEmail)) {
1144
            return false;
1145
        }
1146
        
1147
        $userClass = $this->getUserClass();
1148
        
1149
        if (!is_int($idUsernameEmail)) {
1150
            $usernameEmail = $this->filter->sanitize($idUsernameEmail, ['string', 'trim']);
1151
            $user = $userClass::findFirst([
1152
                'email = :email: or username = :username:',
1153
                'bind' => [
1154
                    'email' => $usernameEmail,
1155
                    'username' => $usernameEmail,
1156
                ],
1157
                'bindTypes' => [
1158
                    'email' => Column::BIND_PARAM_STR,
1159
                    'username' => Column::BIND_PARAM_STR,
1160
                ],
1161
            ]);
1162
        }
1163
        else {
1164
            $user = $userClass::findFirstById((int)$idUsernameEmail);
1165
        }
1166
        
1167
        return $user;
1168
    }
1169
}
1170