Test Failed
Push — master ( 73f7f9...0de326 )
by Julien
04:27
created

Identity::has()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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

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

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

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