Test Failed
Push — master ( a647af...73f7f9 )
by Julien
11:48
created

Identity::getInheritedRoleList()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 12
c 1
b 1
f 0
dl 0
loc 31
ccs 0
cts 13
cp 0
rs 9.8666
cc 4
nc 4
nop 1
crap 20
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' 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
    /**
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
     * @param bool $inherit
368
     *
369
     * @return array
370
     */
371
    public function getIdentity(bool $inherit = true)
372
    {
373
        $user = $this->getUser();
374
        $userAs = $this->getUserAs();
375
        
376
        $roleList = [];
377
        $groupList = [];
378
        $typeList = [];
379
        
380
        if ($user) {
381
            if ($user->rolelist) {
0 ignored issues
show
Bug introduced by
The property rolelist does not exist on true.
Loading history...
382
                foreach ($user->rolelist as $role) {
383
                    $roleList [$role->getIndex()] = $role;
384
                }
385
            }
386
            
387
            if ($user->grouplist) {
0 ignored issues
show
Bug introduced by
The property grouplist does not exist on true.
Loading history...
388
                foreach ($user->grouplist as $group) {
389
                    $groupList [$group->getIndex()] = $group;
390
                    
391
                    if ($group->rolelist) {
392
                        foreach ($group->rolelist as $role) {
393
                            $roleList [$role->getIndex()] = $role;
394
                        }
395
                    }
396
                }
397
            }
398
            
399
            if ($user->typelist) {
0 ignored issues
show
Bug introduced by
The property typelist does not exist on true.
Loading history...
400
                foreach ($user->typelist as $type) {
401
                    $typeList [$type->getIndex()] = $type;
402
                    
403
                    if ($type->grouplist) {
404
                        foreach ($type->grouplist as $group) {
405
                            $groupList [$group->getIndex()] = $group;
406
                            
407
                            if ($group->rolelist) {
408
                                foreach ($group->rolelist as $role) {
409
                                    $roleList [$role->getIndex()] = $role;
410
                                }
411
                            }
412
                        }
413
                    }
414
                }
415
            }
416
        }
417
        
418
        // Append inherit roles
419
        if ($inherit) {
420
            $roleIndexList = [];
421
            foreach ($roleList as $role) {
422
                $roleIndexList []= $role->getIndex();
423
            }
424
            $inheritedRoleIndexList = $this->getInheritedRoleList($roleIndexList);
425
            
426
            /** @var \Phalcon\Mvc\Model\Resultset $inheritedRoleEntity */
427
            $inheritedRoleList = $this->getRoleClass()::find([
428
                'index in ({role:array})', // @todo should filter soft-deleted roles?
429
                'bind' => ['role' => $inheritedRoleIndexList],
430
                'bindTypes' => ['role' => Column::BIND_PARAM_STR],
431
            ]);
432
    
433
            /** @var Models\Role $inheritedRoleEntity */
434
            foreach ($inheritedRoleList as $inheritedRoleEntity) {
435
                $roleList[$inheritedRoleEntity->getIndex()] = $inheritedRoleEntity;
436
            }
437
        }
438
        
439
        // We don't need userAs group / type / role list
440
        return [
441
            'loggedIn' => $this->isLoggedIn(),
442
            'loggedInAs' => $this->isLoggedInAs(),
443
            'user' => $user,
444
            'userAs' => $userAs,
445
            'roleList' => $roleList,
446
            'typeList' => $typeList,
447
            'groupList' => $groupList,
448
        ];
449
    }
450
    
451
    /**
452
     * Return the list of inherited role list (recursively)
453
     *
454
     * @param array $roleIndexList
455
     *
456
     * @return array List of inherited role list (recursive)
457
     */
458
    public function getInheritedRoleList(array $roleIndexList = [])
459
    {
460
        $inheritedRoleList = [];
461
        $processedRoleIndexList = [];
462
        
463
        // While we still have role index list to process
464
        while (!empty($roleIndexList))
465
        {
466
            // Process role index list
467
            foreach ($roleIndexList as $roleIndex)
468
            {
469
                // Get inherited roles from config service
470
                $configRoleList = $this->config->path('permissions.roles.' . $roleIndex . '.inherit', false);
471
                
472
                if ($configRoleList) {
473
                    // Append inherited role to process list
474
                    $roleList = $configRoleList->toArray();
475
                    $roleIndexList = array_merge($roleIndexList, $roleList);
476
                    $inheritedRoleList = array_merge($inheritedRoleList, $roleList);
477
                }
478
                
479
                // Add role index to processed list
480
                $processedRoleIndexList []= $roleIndex;
481
            }
482
            
483
            // Keep the unprocessed role index list
484
            $roleIndexList = array_filter(array_unique(array_diff($roleIndexList, $processedRoleIndexList)));
485
        }
486
        
487
        // Return the list of inherited role list (recursively)
488
        return array_filter(array_unique($inheritedRoleList));
489
    }
490
    
491
    /**
492
     * Return true if the user is currently logged in
493
     *
494
     * @param bool $as
495
     * @param bool $refresh
496
     *
497
     * @return bool
498
     */
499
    public function isLoggedIn($as = false, $refresh = false)
500
    {
501
        return !!$this->getUser($as, $refresh);
502
    }
503
    
504
    /**
505
     * Return true if the user is currently logged in
506
     *
507
     * @param bool $refresh
508
     *
509
     * @return bool
510
     */
511
    public function isLoggedInAs($refresh = false)
512
    {
513
        return $this->isLoggedIn(true, $refresh);
514
    }
515
    
516
    /**
517
     * Get the User related to the current session
518
     *
519
     * @return User|bool
520
     */
521
    public function getUser($as = false, $refresh = false)
522
    {
523
        $property = $as ? 'userAs' : 'user';
524
        
525
        if ($refresh) {
526
            $this->$property = null;
527
        }
528
        
529
        if (is_null($this->$property)) {
530
            
531
            $session = $this->getSession();
532
            
533
            $userClass = $this->getUserClass();
534
            $user = !$session ? false : $userClass::findFirstWithById([
535
                'RoleList',
536
                'GroupList.RoleList',
537
                'TypeList.GroupList.RoleList',
538
//            'GroupList.TypeList.RoleList', // @TODO do it
539
//            'TypeList.RoleList', // @TODO do it
540
            ], $as ? $session->getAsUserId() : $session->getUserId());
541
            
542
            $this->$property = $user ? $user : false;
543
        }
544
        
545
        return $this->$property;
546
    }
547
    
548
    /**
549
     * Get the User As related to the current session
550
     *
551
     * @return bool|User
552
     */
553
    public function getUserAs()
554
    {
555
        return $this->getUser(true);
556
    }
557
    
558
    /**
559
     * Get the "Roles" related to the current session
560
     *
561
     * @param bool $inherit
562
     *
563
     * @return array|mixed
564
     */
565
    public function getRoleList(bool $inherit = true)
566
    {
567
        return $this->getIdentity($inherit)['roleList'] ?? [];
568
    }
569
    
570
    /**
571
     * Get the "Groups" related to the current session
572
     *
573
     * @param bool $inherit
574
     *
575
     * @return array
576
     */
577
    public function getGroupList(bool $inherit = true)
578
    {
579
        return $this->getIdentity($inherit)['groupList'] ?? [];
580
    }
581
    
582
    /**
583
     * Get the "Types" related to the current session
584
     *
585
     * @param bool $inherit
586
     *
587
     * @return array
588
     */
589
    public function getTypeList(bool $inherit = true)
590
    {
591
        return $this->getIdentity($inherit)['typeList'] ?? [];
592
    }
593
    
594
    /**
595
     * @param array|null $roles
596
     *
597
     * @return array
598
     */
599
    public function getAclRoles(array $roleList = null)
600
    {
601
        $roleList ??= $this->getRoleList();
602
        
603
        $aclRoles = [];
604
        $aclRoles['everyone'] = new Role('everyone');
605
        foreach ($roleList as $role) {
606
            if ($role) {
607
                $aclRoles[$role->getIndex()] ??= new Role($role->getIndex());
608
            }
609
        }
610
        
611
        return array_values($aclRoles);
612
    }
613
    
614
    /**
615
     * @param $userId
616
     *
617
     * @return array
618
     */
619
    public function loginAs($params)
620
    {
621
        /** @var Session $session */
622
        $session = $this->getSession();
623
        
624
        $validation = new Validation();
625
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
626
        $validation->validate($params);
627
        
628
        $userId = $session->getUserId();
629
        
630
        if (!empty($userId) && !empty($params['userId'])) {
631
            
632
            if ((int)$params['userId'] === (int)$userId) {
633
                return $this->logoutAs();
634
            }
635
            
636
            $userClass = $this->getUserClass();
637
            $asUser = $userClass::findFirstById((int)$params['userId']);
638
            
639
            if ($asUser) {
640
                if ($this->hasRole(['admin', 'dev'])) {
641
                    $session->setAsUserId($userId);
642
                    $session->setUserId($params['userId']);
643
                }
644
            }
645
            else {
646
                $validation->appendMessage(new Message('User Not Found', 'userId', 'PresenceOf', 404));
647
            }
648
        }
649
        
650
        $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...
651
        foreach ($session->getMessages() as $message) {
652
            $validation->appendMessage($message);
653
        }
654
        
655
        return [
656
            'saved' => $saved,
657
            'loggedIn' => $this->isLoggedIn(false, true),
658
            'loggedInAs' => $this->isLoggedIn(true, true),
659
            'messages' => $validation->getMessages(),
660
        ];
661
    }
662
    
663
    /**
664
     * @return array
665
     */
666
    public function logoutAs()
667
    {
668
        /** @var Session $session */
669
        $session = $this->getSession();
670
        
671
        $asUserId = $session->getAsUserId();
672
        $userId = $session->getUserId();
673
        if (!empty($asUserId) && !empty($userId)) {
674
            $session->setUserId($asUserId);
675
            $session->setAsUserId(null);
676
        }
677
        
678
        return [
679
            '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...
680
            'loggedIn' => $this->isLoggedIn(false, true),
681
            'loggedInAs' => $this->isLoggedIn(true, true),
682
            'messages' => $session->getMessages(),
683
        ];
684
    }
685
    
686
    /**
687
     *
688
     */
689
    public function oauth2(string $provider, int $id, string $accessToken, ?array $meta = [])
690
    {
691
        $loggedInUser = null;
692
        $saved = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $saved is dead and can be removed.
Loading history...
693
        
694
        // retrieve and prepare oauth2 entity
695
        $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...
696
            'provider = :provider: and id = :id:',
697
            'bind' => [
698
                'provider' => $this->filter->sanitize($provider, 'string'),
699
                'id' => (int)$id,
700
            ],
701
            'bindTypes' => [
702
                'provider' => Column::BIND_PARAM_STR,
703
                'id' => Column::BIND_PARAM_INT,
704
            ],
705
        ]);
706
        if (!$oauth2) {
707
            $oauth2 = new Oauth2();
708
            $oauth2->setProviderName($provider);
709
            $oauth2->setProviderId($id);
710
        }
711
        $oauth2->setAccessToken($accessToken);
712
        $oauth2->setMeta($meta);
713
        $oauth2->setName($meta['name'] ?? null);
714
        $oauth2->setFirstName($meta['first_name'] ?? null);
715
        $oauth2->setLastName($meta['last_name'] ?? null);
716
        $oauth2->setEmail($meta['email'] ?? null);
717
        
718
        // ge the current session
719
        $session = $this->getSession();
720
        
721
        // link the current user to the oauth2 entity
722
        $oauth2UserId = $oauth2->getUserId();
723
        $sessionUserId = $session->getUserId();
724
        if (empty($oauth2UserId) && !empty($sessionUserId)) {
725
            $oauth2->setUserId($sessionUserId);
726
        }
727
        
728
        // prepare validation
729
        $validation = new Validation();
730
        
731
        // save the oauth2 entity
732
        $saved = $oauth2->save();
733
        
734
        // append oauth2 error messages
735
        foreach ($oauth2->getMessages() as $message) {
736
            $validation->appendMessage($message);
737
        }
738
        
739
        // a session is required
740
        if (!$session) {
741
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
742
        }
743
        
744
        // user id is required
745
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
746
        $validation->validate($oauth2 ? $oauth2->toArray() : []);
747
        
748
        // All validation passed
749
        if ($saved && !$validation->getMessages()->count()) {
750
            
751
            $user = $this->findUser($oauth2->getUserId());
752
            
753
            // user not found, login failed
754
            if (!$user) {
755
                $validation->appendMessage(new Message('Login Failed', ['id'], 'LoginFailed', 401));
756
            }
757
            
758
            // access forbidden, login failed
759
            else if ($user->isDeleted()) {
760
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
761
            }
762
            
763
            // login success
764
            else {
765
                $loggedInUser = $user;
766
            }
767
            
768
            // Set the oauth user id into the session
769
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
770
            $saved = $session->save();
771
            
772
            // append session error messages
773
            foreach ($session->getMessages() as $message) {
774
                $validation->appendMessage($message);
775
            }
776
        }
777
        
778
        return [
779
            'saved' => $saved,
780
            'loggedIn' => $this->isLoggedIn(false, true),
781
            'loggedInAs' => $this->isLoggedIn(true, true),
782
            'messages' => $validation->getMessages(),
783
        ];
784
    }
785
    
786
    /**
787
     * Login Action
788
     * - Require an active session to bind the logged in userId
789
     *
790
     * @return array
791
     */
792
    public function login(array $params = null)
793
    {
794
        $loggedInUser = null;
795
        $saved = null;
796
        
797
        $session = $this->getSession();
798
        
799
        $validation = new Validation();
800
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
801
        $validation->add('password', new PresenceOf(['message' => 'password is required']));
802
        $validation->validate($params);
803
        
804
        if (!$session) {
805
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
806
        }
807
        
808
        $messages = $validation->getMessages();
809
        
810
        if (!$messages->count()) {
811
            $user = $this->findUser($params['email'] ?? $params['username']);
812
            
813
            if (!$user) {
814
                // user not found, login failed
815
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
816
            }
817
            
818
            else if (empty($user->getPassword())) {
819
                // password disabled, login failed
820
                $validation->appendMessage(new Message('Password Login Disabled', 'password', 'LoginFailed', 401));
821
            }
822
            
823
            else if (!$user->checkPassword($params['password'])) {
824
                // password failed, login failed
825
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
826
            }
827
            
828
            else if ($user->isDeleted()) {
829
                // access forbidden, login failed
830
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
831
            }
832
            
833
            // login success
834
            else {
835
                $loggedInUser = $user;
836
            }
837
            
838
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
839
            $saved = $session->save();
840
            
841
            foreach ($session->getMessages() as $message) {
842
                $validation->appendMessage($message);
843
            }
844
        }
845
        
846
        return [
847
            'saved' => $saved,
848
            'loggedIn' => $this->isLoggedIn(false, true),
849
            'loggedInAs' => $this->isLoggedIn(true, true),
850
            'messages' => $validation->getMessages(),
851
        ];
852
    }
853
    
854
    /**
855
     * Log the user out from the database session
856
     *
857
     * @return bool|mixed|null
858
     */
859
    public function logout()
860
    {
861
        $saved = false;
862
        
863
        $session = $this->getSession();
864
        $validation = new Validation();
865
        $validation->validate();
866
        
867
        if (!$session) {
868
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
869
        }
870
        else {
871
            // Logout
872
            $session->setUserId(null);
873
            $session->setAsUserId(null);
874
            $saved = $session->save();
875
            
876
            foreach ($session->getMessages() as $message) {
877
                $validation->appendMessage($message);
878
            }
879
        }
880
        
881
        return [
882
            'saved' => $saved,
883
            'loggedIn' => $this->isLoggedIn(false, true),
884
            'loggedInAs' => $this->isLoggedIn(true, true),
885
            'messages' => $validation->getMessages(),
886
        ];
887
    }
888
    
889
    /**
890
     * @param array|null $params
891
     *
892
     * @return array
893
     */
894
    public function reset(array $params = null)
895
    {
896
        $saved = false;
897
        $sent = false;
898
        $token = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $token is dead and can be removed.
Loading history...
899
        
900
        $session = $this->getSession();
901
        $validation = new Validation();
902
        
903
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
904
        $validation->validate($params);
905
        
906
        if (!$session) {
907
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
908
        }
909
        else {
910
            $user = false;
911
            if (isset($params['email'])) {
912
                $userClass = $this->getUserClass();
913
                $user = $userClass::findFirstByEmail($params['email']);
914
            }
915
            
916
            // Reset
917
            if ($user) {
918
                
919
                // Password reset request
920
                if (empty($params['token'])) {
921
                    
922
                    // Generate a new token
923
                    $token = $user->prepareToken();
924
                    
925
                    // Send it by email
926
                    $emailClass = $this->getEmailClass();
927
                    
928
                    $email = new $emailClass();
929
                    $email->setViewPath('template/email');
930
                    $email->setTemplateByIndex('reset-password');
931
                    $email->setTo([$user->getEmail()]);
932
                    $meta = [];
933
                    $meta['user'] = $user->expose(['User' => [
934
                        false,
935
                        'firstName',
936
                        'lastName',
937
                        'email',
938
                    ]]);
939
                    $meta['resetLink'] = $this->url->get('/reset-password/' . $token);
940
                    
941
                    $email->setMeta($meta);
942
                    $saved = $user->save();
943
                    $sent = $saved ? $email->send() : false;
944
                    
945
                    // Appending error messages
946
                    foreach (['user', 'email'] as $e) {
947
                        foreach ($$e->getMessages() as $message) {
948
                            $validation->appendMessage($message);
949
                        }
950
                    }
951
                }
952
                
953
                // Password reset
954
                else {
955
                    $validation->add('password', new PresenceOf(['message' => 'password is required']));
956
                    $validation->add('passwordConfirm', new PresenceOf(['message' => 'password confirm is required']));
957
                    $validation->add('password', new Confirmation(['message' => 'password does not match passwordConfirm', 'with' => 'passwordConfirm']));
958
                    $validation->validate($params);
959
                    
960
                    if (!$user->checkToken($params['token'])) {
961
                        $validation->appendMessage(new Message('invalid token', 'token', 'NotValid', 400));
962
                    }
963
                    else {
964
                        if (!count($validation->getMessages())) {
965
                            $params['token'] = null;
966
                            $user->assign($params, ['token', 'password', 'passwordConfirm']);
967
                            $saved = $user->save();
968
                        }
969
                    }
970
                }
971
                
972
                // Appending error messages
973
                foreach ($user->getMessages() as $message) {
974
                    $validation->appendMessage($message);
975
                }
976
            }
977
            else {
978
//                $validation->appendMessage(new Message('User not found', 'user', 'PresenceOf', 404));
979
                // OWASP Protect User Enumeration
980
                $saved = true;
981
                $sent = true;
982
            }
983
        }
984
        
985
        return [
986
            'saved' => $saved,
987
            'sent' => $sent,
988
            'messages' => $validation->getMessages(),
989
        ];
990
    }
991
    
992
    /**
993
     * Get key / token fields to use for the session fetch & validation
994
     *
995
     * @return array
996
     */
997
    public function getKeyToken()
998
    {
999
        $basicAuth = $this->request->getBasicAuth();
1000
        $authorization = array_filter(explode(' ', $this->request->getHeader('Authorization') ? : ''));
1001
        
1002
        $jwt = $this->request->get('jwt', 'string');
1003
        $key = $this->request->get('key', 'string');
1004
        $token = $this->request->get('token', 'string');
1005
        
1006
        if (!empty($jwt)) {
1007
            $sessionClaim = $this->getClaim($jwt, $this->sessionKey);
1008
            $key = $sessionClaim->key ?? null;
1009
            $token = $sessionClaim->token ?? null;
1010
        }
1011
        
1012
        else if (!empty($basicAuth)) {
1013
            $key = $basicAuth['username'] ?? null;
1014
            $token = $basicAuth['password'] ?? null;
1015
        }
1016
        
1017
        else if (!empty($authorization)) {
1018
            $authorizationType = $authorization[0] ?? 'Bearer';
1019
            $authorizationToken = $authorization[1] ?? null;
1020
            
1021
            if (strtolower($authorizationType) === 'bearer') {
1022
                $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

1022
                $sessionClaim = $this->getClaim(/** @scrutinizer ignore-type */ $authorizationToken, $this->sessionKey);
Loading history...
1023
                $key = $sessionClaim->key ?? null;
1024
                $token = $sessionClaim->token ?? null;
1025
            }
1026
        }
1027
        
1028
        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

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

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