Test Failed
Push — master ( f4fe2e...51765d )
by Julien
10:58
created

Identity::getJwt()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 33
ccs 0
cts 23
cp 0
rs 8.9297
cc 6
nc 8
nop 1
crap 42
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 array
72
     */
73
    public $store = [];
74
    
75
    /**
76
     * @var User
77
     */
78
    public $user;
79
    
80
    /**
81
     * @var User
82
     */
83
    public $userAs;
84
    
85
    /**
86
     * @var Session
87
     */
88
    public $currentSession;
89
    
90
    /**
91
     * @var string|int|bool|null
92
     */
93
    public $identity;
94
    
95
    public function __construct($options = [])
96
    {
97
        $this->setOptions($options);
98
        $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

98
        $this->sessionKey = $this->getOption('sessionKey', /** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
99
        $this->setMode($this->getOption('mode', $this->mode));
100
//        $this->set($this->getFromSession());
101
    }
102
    
103
    /**
104
     * Set default options
105
     *
106
     * @param array $options
107
     */
108
    public function setOptions($options = [])
109
    {
110
        $this->options = $options;
111
    }
112
    
113
    /**
114
     * Getting an option value from the key, allowing to specify a default value
115
     *
116
     * @param $key
117
     * @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...
118
     *
119
     * @return mixed|null
120
     */
121
    public function getOption($key, $default = null)
122
    {
123
        return $this->options[$key] ?? $default;
124
    }
125
    
126
    /**
127
     * @return string|Mvc\Model|\Zemit\Models\Session
128
     */
129
    public function getSessionClass()
130
    {
131
        return $this->config->getModelClass(\Zemit\Models\Session::class);
132
    }
133
    
134
    /**
135
     * @return string|Mvc\Model|\Zemit\Models\User
136
     */
137
    public function getUserClass()
138
    {
139
        return $this->config->getModelClass(\Zemit\Models\User::class);
140
    }
141
    
142
    /**
143
     * @return string|Mvc\Model|\Zemit\Models\Group
144
     */
145
    public function getGroupClass()
146
    {
147
        return $this->config->getModelClass(\Zemit\Models\Group::class);
148
    }
149
    
150
    /**
151
     * @return string|Mvc\Model|\Zemit\Models\Role
152
     */
153
    public function getRoleClass()
154
    {
155
        return $this->config->getModelClass(\Zemit\Models\Role::class);
156
    }
157
    
158
    /**
159
     * @return string|Mvc\Model|\Zemit\Models\Type
160
     */
161
    public function getTypeClass()
162
    {
163
        return $this->config->getModelClass(\Zemit\Models\Type::class);
164
    }
165
    
166
    /**
167
     * @return string
168
     */
169
    public function getEmailClass()
170
    {
171
        return $this->config->getModelClass(\Zemit\Models\Email::class);
172
    }
173
    
174
    /**
175
     * Get the current mode
176
     * @return string
177
     */
178
    public function getMode(): string
179
    {
180
        return $this->mode;
181
    }
182
    
183
    /**
184
     * Set the mode
185
     *
186
     * @param string $mode
187
     *
188
     * @throws \Exception Throw an exception if the mode is not supported
189
     */
190
    public function setMode($mode)
191
    {
192
        switch ($mode) {
193
            case self::MODE_STRING:
194
            case self::MODE_JWT:
195
                $this->mode = $mode;
196
                break;
197
            default:
198
                throw new \Exception('Identity mode `' . $mode . '` is not supported.');
199
                break;
200
        }
201
    }
202
    
203
    /**
204
     * @return bool|mixed
205
     */
206
    public function getFromSession()
207
    {
208
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
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

208
        $ret = $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
Loading history...
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

208
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey) : null;
Loading history...
209
        
210
        if ($ret) {
211
            switch ($this->mode) {
212
                case self::MODE_DEFAULT:
213
                    break;
214
                case self::MODE_JWT:
215
                    $ret = $this->jwt->parseToken($ret)->getClaim('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...
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

215
                    $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...
216
                    break;
217
            }
218
        }
219
        
220
        return json_decode($ret);
221
    }
222
    
223
    /**
224
     * Save an identity into the session
225
     *
226
     * @param int|string|null $identity
227
     */
228
    public function setIntoSession($identity)
229
    {
230
        
231
        $identity = json_encode($identity);
232
        
233
        $token = null;
234
        switch ($this->mode) {
235
            case self::MODE_JWT:
236
                $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...
237
                break;
238
        }
239
        
240
        $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

240
        $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $token ?: $identity);
Loading history...
241
    }
242
    
243
    /**
244
     * Set an identity
245
     *
246
     * @param int|string|null $identity
247
     */
248
    public function set($identity)
249
    {
250
        $this->setIntoSession($identity);
251
        $this->identity = $identity;
252
    }
253
    
254
    /**
255
     * Get the current identity
256
     * @return int|string|null
257
     */
258
    public function get()
259
    {
260
        $this->identity ??= $this->getFromSession();
261
        
262
        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...
263
    }
264
    
265
    /**
266
     * @param array|null $roles
267
     * @param bool $or
268
     * @param bool $inherit
269
     *
270
     * @return bool
271
     */
272
    public function hasRole(?array $roles = null, bool $or = false, bool $inherit = true)
273
    {
274
        return $this->has($roles, array_keys($this->getRoleList($inherit) ?: []), $or);
275
    }
276
    
277
    /**
278
     * Return the current user ID
279
     *
280
     * @return string|int|bool
281
     */
282
    public function getUserId($as = false)
283
    {
284
        /** @var User $user */
285
        $user = $this->getUser($as);
286
        
287
        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...
288
    }
289
    
290
    /**
291
     * Check if the needles meet the haystack using nested arrays
292
     * Reversing ANDs and ORs within each nested subarray
293
     *
294
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), true); // 'dev' OR 'admin'
295
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), false); // 'dev' AND 'admin'
296
     *
297
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles()); // 'dev' AND 'admin'
298
     * $this->has([['dev', 'admin']], $this->getUser()->getRoles()); // 'dev' OR 'admin'
299
     * $this->has([[['dev', 'admin']]], $this->getUser()->getRoles()); // 'dev' AND 'admin'
300
     *
301
     * @param array|string|null $needles Needles to match and meet the rules
302
     * @param array $haystack Haystack array to search into
303
     * @param bool $or True to force with "OR" , false to force "AND" condition
304
     *
305
     * @return bool Return true or false if the needles rules are being met
306
     */
307
    public function has($needles = null, array $haystack = [], $or = false)
308
    {
309
        if (!is_array($needles)) {
310
            $needles = [$needles];
311
        }
312
        
313
        $result = [];
314
        foreach ([...$needles] as $needle) {
315
            if (is_array($needle)) {
316
                $result [] = $this->has($needle, $haystack, !$or);
317
            }
318
            else {
319
                $result [] = in_array($needle, $haystack, true);
320
            }
321
        }
322
        
323
        return $or ?
324
            !in_array(false, $result, true) :
325
            in_array(true, $result, true);
326
    }
327
    
328
    /**
329
     * Create a refresh a session
330
     *
331
     * @param bool $refresh
332
     *
333
     * @throws \Phalcon\Security\Exception
334
     */
335
    public function getJwt(bool $refresh = false)
336
    {
337
        [$key, $token] = $this->getKeyToken();
338
        
339
        $key ??= $this->security->getRandom()->uuid();
340
        $token ??= $this->security->getRandom()->hex(512);
341
        $newToken = $refresh? $this->security->getRandom()->hex(512) : $token;
342
        $date = date('Y-m-d H:i:s');
343
        
344
        $sessionClass = $this->getSessionClass();
345
        $session = $this->getSession($key, $token) ?: new $sessionClass();
346
        $session->setKey($key);
347
        $session->setToken($session->hash($key . $newToken));
348
        $session->setDate($date);
349
        $saved = $session->save();
350
        
351
        // store key & token to this instance
352
        $this->store = ['key' => $session->getKey(), 'token' => $newToken];
353
        
354
        // store key & token into the session
355
        if ($this->config->path('identity.sessionFallback', false) && $saved) {
356
            $this->session->set($this->sessionKey, $this->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

356
            $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $this->store);
Loading history...
357
        } else {
358
            $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

358
            $this->session->remove(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
359
        }
360
        
361
        return [
362
            'saved' => $saved,
363
            'hasSession' => $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

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

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

782
            else if ($user->/** @scrutinizer ignore-call */ isDeleted()) {
Loading history...
783
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
784
            }
785
            
786
            // login success
787
            else {
788
                $loggedInUser = $user;
789
            }
790
            
791
            // Set the oauth user id into the session
792
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
0 ignored issues
show
Bug introduced by
The method getId() does not exist on Phalcon\Mvc\Model\ResultInterface. It seems like you code against a sub-type of Phalcon\Mvc\Model\ResultInterface such as Phalcon\Mvc\Model. ( Ignorable by Annotation )

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

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

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

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

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

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

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

1020
    public function getKeyToken(string $jwt = null, /** @scrutinizer ignore-unused */ string $key = null, string $token = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $token is not used and could be removed. ( Ignorable by Annotation )

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

1020
    public function getKeyToken(string $jwt = null, string $key = null, /** @scrutinizer ignore-unused */ string $token = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1021
    {
1022
        $basicAuth = $this->request->getBasicAuth();
1023
        $authorization = array_filter(explode(' ', $this->request->getHeader(
1024
            $this->config->path('identity.authorizationHeader', 'Authorization')
1025
        ) ?: ''));
1026
        
1027
        $jwt = $this->request->get('jwt', 'string', $jwt);
1028
        $key = $this->request->get('key', 'string', $this->store['key'] ?? null);
1029
        $token = $this->request->get('token', 'string', $this->store['token'] ?? null);
1030
        
1031
        if (!empty($key) && !empty($token)) {
1032
        
1033
        }
1034
        
1035
        else if (!empty($jwt)) {
1036
            $sessionClaim = $this->getClaim($jwt, $this->sessionKey);
1037
            $key = $sessionClaim->key ?? null;
1038
            $token = $sessionClaim->token ?? null;
1039
        }
1040
        
1041
        else if (!empty($basicAuth)) {
1042
            $key = $basicAuth['username'] ?? null;
1043
            $token = $basicAuth['password'] ?? null;
1044
        }
1045
        
1046
        else if (!empty($authorization)) {
1047
            $authorizationType = $authorization[0] ?? 'Bearer';
1048
            $authorizationToken = $authorization[1] ?? null;
1049
            
1050
            if (strtolower($authorizationType) === 'bearer') {
1051
                $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

1051
                $sessionClaim = $this->getClaim(/** @scrutinizer ignore-type */ $authorizationToken, $this->sessionKey);
Loading history...
1052
                $key = $sessionClaim->key ?? null;
1053
                $token = $sessionClaim->token ?? null;
1054
            }
1055
        }
1056
        
1057
        else if (
1058
            $this->config->path('identity.sessionFallback', false) &&
1059
            $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

1059
            $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey)
Loading history...
1060
        ) {
1061
            $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

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