Test Failed
Push — master ( 51765d...a30637 )
by Julien
03:53
created

Identity::setMode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 8
c 2
b 0
f 0
dl 0
loc 10
ccs 0
cts 7
cp 0
rs 10
cc 3
nc 3
nop 1
crap 12
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
use Zemit\Support\ModelsMap;
25
use Zemit\Support\Options;
26
use Zemit\Support\OptionsInterface;
27
28
/**
29
 * Class Identity
30
 * {@inheritDoc}
31
 *
32
 * @author Julien Turbide <[email protected]>
33
 * @copyright Zemit Team <[email protected]>
34
 *
35
 * @since 1.0
36
 * @version 1.0
37
 *
38
 * @package Zemit
39
 */
40
class Identity extends Injectable implements OptionsInterface
41
{
42
    use Options;
43
    use ModelsMap;
44
    
45
    /**
46
     * Without encryption
47
     */
48
    const MODE_DEFAULT = self::MODE_JWT;
49
    
50
    /**
51
     * Without encryption (raw string into the session)
52
     */
53
    const MODE_STRING = 'string';
54
    
55
    /**
56
     * Store using JWT (jwt encrypted into the session)
57
     */
58
    const MODE_JWT = 'jwt';
59
    
60
    /**
61
     * Locale mode for the prepare fonction
62
     * @var string
63
     */
64
    public string $mode = self::MODE_DEFAULT;
65
    
66
    /**
67
     * @var string
68
     */
69
    public string $sessionKey = 'zemit-identity';
70
    
71
    /**
72
     * @var array
73
     */
74
    public $store = [];
75
    
76
    /**
77
     * @var User
78
     */
79
    public $user;
80
    
81
    /**
82
     * @var User
83
     */
84
    public $userAs;
85
    
86
    /**
87
     * @var Session
88
     */
89
    public $currentSession;
90
    
91
    /**
92
     * @var string|int|bool|null
93
     */
94
    public $identity;
95
    
96
    /**
97
     * @return void
98
     * @throws \Exception
99
     */
100
    public function init()
101
    {
102
        $this->sessionKey = $this->getOption('sessionKey', $this->sessionKey);
103
        $this->setMode($this->getOption('mode', $this->mode));
104
    }
105
    
106
    /**
107
     * Get the current mode
108
     * @return string
109
     */
110
    public function getMode(): string
111
    {
112
        return $this->mode;
113
    }
114
    
115
    /**
116
     * Set the mode
117
     *
118
     * @param string $mode
119
     *
120
     * @throws \Exception Throw an exception if the mode is not supported
121
     */
122
    public function setMode($mode)
123
    {
124
        switch ($mode) {
125
            case self::MODE_STRING:
126
            case self::MODE_JWT:
127
                $this->mode = $mode;
128
                break;
129
            default:
130
                throw new \Exception('Identity mode `' . $mode . '` is not supported.');
131
                break;
132
        }
133
    }
134
    
135
    /**
136
     * @return bool|mixed
137
     */
138
    public function getFromSession()
139
    {
140
        $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...
141
        
142
        if ($ret) {
143
            switch ($this->mode) {
144
                case self::MODE_DEFAULT:
145
                    break;
146
                case self::MODE_JWT:
147
                    $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

147
                    $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...
148
                    break;
149
            }
150
        }
151
        
152
        return json_decode($ret);
153
    }
154
    
155
    /**
156
     * Save an identity into the session
157
     *
158
     * @param int|string|null $identity
159
     */
160
    public function setIntoSession($identity)
161
    {
162
        
163
        $identity = json_encode($identity);
164
        
165
        $token = null;
166
        switch ($this->mode) {
167
            case self::MODE_JWT:
168
                $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...
169
                break;
170
        }
171
        
172
        $this->session->set($this->sessionKey, $token ?: $identity);
173
    }
174
    
175
    /**
176
     * Set an identity
177
     *
178
     * @param int|string|null $identity
179
     */
180
    public function set($identity)
181
    {
182
        $this->setIntoSession($identity);
183
        $this->identity = $identity;
184
    }
185
    
186
    /**
187
     * Get the current identity
188
     * @return int|string|null
189
     */
190
    public function get()
191
    {
192
        $this->identity ??= $this->getFromSession();
193
        
194
        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...
195
    }
196
    
197
    /**
198
     * @param array|null $roles
199
     * @param bool $or
200
     * @param bool $inherit
201
     *
202
     * @return bool
203
     */
204
    public function hasRole(?array $roles = null, bool $or = false, bool $inherit = true)
205
    {
206
        return $this->has($roles, array_keys($this->getRoleList($inherit) ?: []), $or);
207
    }
208
    
209
    /**
210
     * Return the current user ID
211
     *
212
     * @return string|int|bool
213
     */
214
    public function getUserId($as = false)
215
    {
216
        /** @var User $user */
217
        $user = $this->getUser($as);
218
        
219
        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...
220
    }
221
    
222
    /**
223
     * Check if the needles meet the haystack using nested arrays
224
     * Reversing ANDs and ORs within each nested subarray
225
     *
226
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), true); // 'dev' OR 'admin'
227
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), false); // 'dev' AND 'admin'
228
     *
229
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles()); // 'dev' AND 'admin'
230
     * $this->has([['dev', 'admin']], $this->getUser()->getRoles()); // 'dev' OR 'admin'
231
     * $this->has([[['dev', 'admin']]], $this->getUser()->getRoles()); // 'dev' AND 'admin'
232
     *
233
     * @param array|string|null $needles Needles to match and meet the rules
234
     * @param array $haystack Haystack array to search into
235
     * @param bool $or True to force with "OR" , false to force "AND" condition
236
     *
237
     * @return bool Return true or false if the needles rules are being met
238
     */
239
    public function has($needles = null, array $haystack = [], $or = false)
240
    {
241
        if (!is_array($needles)) {
242
            $needles = [$needles];
243
        }
244
        
245
        $result = [];
246
        foreach ([...$needles] as $needle) {
247
            if (is_array($needle)) {
248
                $result [] = $this->has($needle, $haystack, !$or);
249
            }
250
            else {
251
                $result [] = in_array($needle, $haystack, true);
252
            }
253
        }
254
        
255
        return $or ?
256
            !in_array(false, $result, true) :
257
            in_array(true, $result, true);
258
    }
259
    
260
    /**
261
     * Create a refresh a session
262
     *
263
     * @param bool $refresh
264
     *
265
     * @throws \Phalcon\Security\Exception
266
     */
267
    public function getJwt(bool $refresh = false)
268
    {
269
        [$key, $token] = $this->getKeyToken();
270
        
271
        $key ??= $this->security->getRandom()->uuid();
272
        $token ??= $this->security->getRandom()->hex(512);
273
        $newToken = $refresh ? $this->security->getRandom()->hex(512) : $token;
274
        $date = date('Y-m-d H:i:s');
275
        
276
        $sessionClass = $this->getSessionClass();
277
        $session = $this->getSession($key, $token) ?: new $sessionClass();
278
        $session->setKey($key);
279
        $session->setToken($session->hash($key . $newToken));
280
        $session->setDate($date);
281
        $saved = $session->save();
282
        
283
        // store key & token to this instance
284
        $this->store = ['key' => $session->getKey(), 'token' => $newToken];
285
        
286
        // store key & token into the session
287
        if ($this->config->path('identity.sessionFallback', false) && $saved) {
288
            $this->session->set($this->sessionKey, $this->store);
289
        }
290
        else {
291
            $this->session->remove($this->sessionKey);
292
        }
293
        
294
        return [
295
            'saved' => $saved,
296
            'hasSession' => $this->session->has($this->sessionKey),
297
            'refreshed' => $saved && $refresh,
298
            'validated' => $session->checkHash($session->getToken(), $session->getKey() . $token),
299
            'messages' => $session->getMessages(),
300
            'jwt' => $this->getJwtToken($this->sessionKey, $this->store),
301
        ];
302
    }
303
    
304
    /**
305
     * Get basic Identity information
306
     *
307
     * @param bool $inherit
308
     *
309
     * @return array
310
     */
311
    public function getIdentity(bool $inherit = true)
312
    {
313
        $user = $this->getUser();
314
        $userAs = $this->getUserAs();
315
        
316
        $roleList = [];
317
        $groupList = [];
318
        $typeList = [];
319
        
320
        if ($user) {
321
            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...
322
                foreach ($user->rolelist as $role) {
323
                    $roleList [$role->getIndex()] = $role;
324
                }
325
            }
326
            
327
            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...
328
                foreach ($user->grouplist as $group) {
329
                    $groupList [$group->getIndex()] = $group;
330
                    
331
                    if ($group->rolelist) {
332
                        foreach ($group->rolelist as $role) {
333
                            $roleList [$role->getIndex()] = $role;
334
                        }
335
                    }
336
                }
337
            }
338
            
339
            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...
340
                foreach ($user->typelist as $type) {
341
                    $typeList [$type->getIndex()] = $type;
342
                    
343
                    if ($type->grouplist) {
344
                        foreach ($type->grouplist as $group) {
345
                            $groupList [$group->getIndex()] = $group;
346
                            
347
                            if ($group->rolelist) {
348
                                foreach ($group->rolelist as $role) {
349
                                    $roleList [$role->getIndex()] = $role;
350
                                }
351
                            }
352
                        }
353
                    }
354
                }
355
            }
356
        }
357
        
358
        // Append inherit roles
359
        if ($inherit) {
360
            $roleIndexList = [];
361
            foreach ($roleList as $role) {
362
                $roleIndexList [] = $role->getIndex();
363
            }
364
            
365
            $inheritedRoleIndexList = $this->getInheritedRoleList($roleIndexList);
366
            if (!empty($inheritedRoleIndexList)) {
367
                
368
                /** @var \Phalcon\Mvc\Model\Resultset $inheritedRoleEntity */
369
                $inheritedRoleList = $this->getRoleClass()::find([
370
                    'index in ({role:array})', // @todo should filter soft-deleted roles?
371
                    'bind' => ['role' => $inheritedRoleIndexList],
372
                    'bindTypes' => ['role' => Column::BIND_PARAM_STR],
373
                ]);
374
                
375
                /** @var Models\Role $inheritedRoleEntity */
376
                foreach ($inheritedRoleList as $inheritedRoleEntity) {
377
                    $roleList[$inheritedRoleEntity->getIndex()] = $inheritedRoleEntity;
378
                }
379
            }
380
        }
381
        
382
        // We don't need userAs group / type / role list
383
        return [
384
            'loggedIn' => $this->isLoggedIn(),
385
            'loggedInAs' => $this->isLoggedInAs(),
386
            'user' => $user,
387
            'userAs' => $userAs,
388
            'roleList' => $roleList,
389
            'typeList' => $typeList,
390
            'groupList' => $groupList,
391
        ];
392
    }
393
    
394
    /**
395
     * Return the list of inherited role list (recursively)
396
     *
397
     * @param array $roleIndexList
398
     *
399
     * @return array List of inherited role list (recursive)
400
     */
401
    public function getInheritedRoleList(array $roleIndexList = [])
402
    {
403
        $inheritedRoleList = [];
404
        $processedRoleIndexList = [];
405
        
406
        // While we still have role index list to process
407
        while (!empty($roleIndexList)) {
408
            // Process role index list
409
            foreach ($roleIndexList as $roleIndex) {
410
                // Get inherited roles from config service
411
                $configRoleList = $this->config->path('permissions.roles.' . $roleIndex . '.inherit', false);
412
                
413
                if ($configRoleList) {
414
                    // Append inherited role to process list
415
                    $roleList = $configRoleList->toArray();
416
                    $roleIndexList = array_merge($roleIndexList, $roleList);
417
                    $inheritedRoleList = array_merge($inheritedRoleList, $roleList);
418
                }
419
                
420
                // Add role index to processed list
421
                $processedRoleIndexList [] = $roleIndex;
422
            }
423
            
424
            // Keep the unprocessed role index list
425
            $roleIndexList = array_filter(array_unique(array_diff($roleIndexList, $processedRoleIndexList)));
426
        }
427
        
428
        // Return the list of inherited role list (recursively)
429
        return array_values(array_filter(array_unique($inheritedRoleList)));
430
    }
431
    
432
    /**
433
     * Return true if the user is currently logged in
434
     *
435
     * @param bool $as
436
     * @param bool $refresh
437
     *
438
     * @return bool
439
     */
440
    public function isLoggedIn($as = false, $refresh = false)
441
    {
442
        return !!$this->getUser($as, $refresh);
443
    }
444
    
445
    /**
446
     * Return true if the user is currently logged in
447
     *
448
     * @param bool $refresh
449
     *
450
     * @return bool
451
     */
452
    public function isLoggedInAs($refresh = false)
453
    {
454
        return $this->isLoggedIn(true, $refresh);
455
    }
456
    
457
    /**
458
     * Get the User related to the current session
459
     *
460
     * @return User|bool
461
     */
462
    public function getUser($as = false, $refresh = false)
463
    {
464
        $property = $as ? 'userAs' : 'user';
465
        
466
        if ($refresh) {
467
            $this->$property = null;
468
        }
469
        
470
        if (is_null($this->$property)) {
471
            
472
            $session = $this->getSession();
473
            
474
            $userClass = $this->getUserClass();
475
            $user = !$session ? false : $userClass::findFirstWithById([
476
                'RoleList',
477
                'GroupList.RoleList',
478
                'TypeList.GroupList.RoleList',
479
//            'GroupList.TypeList.RoleList', // @TODO do it
480
//            'TypeList.RoleList', // @TODO do it
481
            ], $as ? $session->getAsUserId() : $session->getUserId());
482
            
483
            $this->$property = $user ? $user : false;
484
        }
485
        
486
        return $this->$property;
487
    }
488
    
489
    /**
490
     * Get the User As related to the current session
491
     *
492
     * @return bool|User
493
     */
494
    public function getUserAs()
495
    {
496
        return $this->getUser(true);
497
    }
498
    
499
    /**
500
     * Get the "Roles" related to the current session
501
     *
502
     * @param bool $inherit
503
     *
504
     * @return array|mixed
505
     */
506
    public function getRoleList(bool $inherit = true)
507
    {
508
        return $this->getIdentity($inherit)['roleList'] ?? [];
509
    }
510
    
511
    /**
512
     * Get the "Groups" related to the current session
513
     *
514
     * @param bool $inherit
515
     *
516
     * @return array
517
     */
518
    public function getGroupList(bool $inherit = true)
519
    {
520
        return $this->getIdentity($inherit)['groupList'] ?? [];
521
    }
522
    
523
    /**
524
     * Get the "Types" related to the current session
525
     *
526
     * @param bool $inherit
527
     *
528
     * @return array
529
     */
530
    public function getTypeList(bool $inherit = true)
531
    {
532
        return $this->getIdentity($inherit)['typeList'] ?? [];
533
    }
534
    
535
    /**
536
     * Return the list of ACL roles
537
     * - Reserved roles: guest, cli, everyone
538
     *
539
     * @param array|null $roleList
540
     * @return array
541
     */
542
    public function getAclRoles(?array $roleList = null): array
543
    {
544
        $roleList ??= $this->getRoleList();
545
        $aclRoles = [];
546
        
547
        // Add roles from databases
548
        foreach ($roleList as $role) {
549
            if ($role) {
550
                $aclRoles[$role->getIndex()] ??= new Role($role->getIndex());
551
            }
552
        }
553
        
554
        // Add guest role if no roles was detected
555
        if (count($aclRoles) === 0) {
556
            $aclRoles['guest'] = new Role('guest', 'Guest without role');
557
        }
558
        
559
        // Add console role
560
        if ($this->bootstrap->isConsole()) {
561
            $aclRoles['cli'] = new Role('cli', 'Console mode');
562
        }
563
        
564
        // Add everyone role
565
        $aclRoles['everyone'] = new Role('everyone', 'Everyone');
566
        
567
        return array_filter(array_values(array_unique($aclRoles)));
568
    }
569
    
570
    /**
571
     * @param $userId
572
     *
573
     * @return array
574
     */
575
    public function loginAs($params)
576
    {
577
        /** @var Session $session */
578
        $session = $this->getSession();
579
        
580
        $validation = new Validation();
581
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
582
        $validation->validate($params);
583
        
584
        $userId = $session->getUserId();
585
        
586
        if (!empty($userId) && !empty($params['userId'])) {
587
            
588
            if ((int)$params['userId'] === (int)$userId) {
589
                return $this->logoutAs();
590
            }
591
            
592
            $userClass = $this->getUserClass();
593
            $asUser = $userClass::findFirstById((int)$params['userId']);
594
            
595
            if ($asUser) {
596
                if ($this->hasRole(['admin', 'dev'])) {
597
                    $session->setAsUserId($userId);
598
                    $session->setUserId($params['userId']);
599
                }
600
            }
601
            else {
602
                $validation->appendMessage(new Message('User Not Found', 'userId', 'PresenceOf', 404));
603
            }
604
        }
605
        
606
        $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...
607
        foreach ($session->getMessages() as $message) {
608
            $validation->appendMessage($message);
609
        }
610
        
611
        return [
612
            'saved' => $saved,
613
            'loggedIn' => $this->isLoggedIn(false, true),
614
            'loggedInAs' => $this->isLoggedIn(true, true),
615
            'messages' => $validation->getMessages(),
616
        ];
617
    }
618
    
619
    /**
620
     * @return array
621
     */
622
    public function logoutAs()
623
    {
624
        /** @var Session $session */
625
        $session = $this->getSession();
626
        
627
        $asUserId = $session->getAsUserId();
628
        $userId = $session->getUserId();
629
        if (!empty($asUserId) && !empty($userId)) {
630
            $session->setUserId($asUserId);
631
            $session->setAsUserId(null);
632
        }
633
        
634
        return [
635
            '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...
636
            'loggedIn' => $this->isLoggedIn(false, true),
637
            'loggedInAs' => $this->isLoggedIn(true, true),
638
            'messages' => $session->getMessages(),
639
        ];
640
    }
641
    
642
    /**
643
     *
644
     */
645
    public function oauth2(string $provider, int $id, string $accessToken, ?array $meta = [])
646
    {
647
        $loggedInUser = null;
648
        $saved = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $saved is dead and can be removed.
Loading history...
649
        
650
        // retrieve and prepare oauth2 entity
651
        $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...
652
            'provider = :provider: and id = :id:',
653
            'bind' => [
654
                'provider' => $this->filter->sanitize($provider, 'string'),
655
                'id' => (int)$id,
656
            ],
657
            'bindTypes' => [
658
                'provider' => Column::BIND_PARAM_STR,
659
                'id' => Column::BIND_PARAM_INT,
660
            ],
661
        ]);
662
        if (!$oauth2) {
663
            $oauth2 = new Oauth2();
664
            $oauth2->setProviderName($provider);
665
            $oauth2->setProviderId($id);
666
        }
667
        $oauth2->setAccessToken($accessToken);
668
        $oauth2->setMeta($meta);
669
        $oauth2->setName($meta['name'] ?? null);
670
        $oauth2->setFirstName($meta['first_name'] ?? null);
671
        $oauth2->setLastName($meta['last_name'] ?? null);
672
        $oauth2->setEmail($meta['email'] ?? null);
673
        
674
        // get the current session
675
        $session = $this->getSession();
676
        
677
        // link the current user to the oauth2 entity
678
        $oauth2UserId = $oauth2->getUserId();
679
        $sessionUserId = $session->getUserId();
680
        if (empty($oauth2UserId) && !empty($sessionUserId)) {
681
            $oauth2->setUserId($sessionUserId);
682
        }
683
        
684
        // prepare validation
685
        $validation = new Validation();
686
        
687
        // save the oauth2 entity
688
        $saved = $oauth2->save();
689
        
690
        // append oauth2 error messages
691
        foreach ($oauth2->getMessages() as $message) {
692
            $validation->appendMessage($message);
693
        }
694
        
695
        // a session is required
696
        if (!$session) {
697
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
698
        }
699
        
700
        // user id is required
701
        $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
702
        $validation->validate($oauth2 ? $oauth2->toArray() : []);
703
        
704
        // All validation passed
705
        if ($saved && !$validation->getMessages()->count()) {
706
            
707
            $user = $this->findUser($oauth2->getUserId());
708
            
709
            // user not found, login failed
710
            if (!$user) {
711
                $validation->appendMessage(new Message('Login Failed', ['id'], 'LoginFailed', 401));
712
            }
713
            
714
            // access forbidden, login failed
715
            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

715
            else if ($user->/** @scrutinizer ignore-call */ isDeleted()) {
Loading history...
716
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
717
            }
718
            
719
            // login success
720
            else {
721
                $loggedInUser = $user;
722
            }
723
            
724
            // Set the oauth user id into the session
725
            $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

725
            $session->setUserId($loggedInUser ? $loggedInUser->/** @scrutinizer ignore-call */ getId() : null);
Loading history...
726
            $saved = $session->save();
727
            
728
            // append session error messages
729
            foreach ($session->getMessages() as $message) {
730
                $validation->appendMessage($message);
731
            }
732
        }
733
        
734
        return [
735
            'saved' => $saved,
736
            'loggedIn' => $this->isLoggedIn(false, true),
737
            'loggedInAs' => $this->isLoggedIn(true, true),
738
            'messages' => $validation->getMessages(),
739
        ];
740
    }
741
    
742
    /**
743
     * Login Action
744
     * - Require an active session to bind the logged in userId
745
     *
746
     * @return array
747
     */
748
    public function login(array $params = null)
749
    {
750
        $loggedInUser = null;
751
        $saved = null;
752
        
753
        $session = $this->getSession();
754
        
755
        $validation = new Validation();
756
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
757
        $validation->add('password', new PresenceOf(['message' => 'password is required']));
758
        $validation->validate($params);
759
        
760
        if (!$session) {
761
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
762
        }
763
        
764
        $messages = $validation->getMessages();
765
        
766
        if (!$messages->count()) {
767
            $user = $this->findUser($params['email'] ?? $params['username']);
768
            
769
            if (!$user) {
770
                // user not found, login failed
771
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
772
            }
773
            
774
            else if ($user->isDeleted()) {
775
                // access forbidden, login failed
776
                $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
777
            }
778
            
779
            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

779
            else if (empty($user->/** @scrutinizer ignore-call */ getPassword())) {
Loading history...
780
                // password disabled, login failed
781
                $validation->appendMessage(new Message('Password Login Disabled', 'password', 'LoginFailed', 401));
782
            }
783
            
784
            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

784
            else if (!$user->/** @scrutinizer ignore-call */ checkPassword($params['password'])) {
Loading history...
785
                // password failed, login failed
786
                $validation->appendMessage(new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401));
787
            }
788
            
789
            // login success
790
            else {
791
                $loggedInUser = $user;
792
            }
793
            
794
            $session->setUserId($loggedInUser ? $loggedInUser->getId() : null);
795
            $saved = $session->save();
796
            
797
            foreach ($session->getMessages() as $message) {
798
                $validation->appendMessage($message);
799
            }
800
        }
801
        
802
        return [
803
            'saved' => $saved,
804
            'loggedIn' => $this->isLoggedIn(false, true),
805
            'loggedInAs' => $this->isLoggedIn(true, true),
806
            'messages' => $validation->getMessages(),
807
        ];
808
    }
809
    
810
    /**
811
     * Log the user out from the database session
812
     *
813
     * @return bool|mixed|null
814
     */
815
    public function logout()
816
    {
817
        $saved = false;
818
        
819
        $sessionEntity = $this->getSession();
820
        $validation = new Validation();
821
        $validation->validate();
822
        
823
        if (!$sessionEntity) {
824
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
825
        }
826
        else {
827
            // Logout
828
            $sessionEntity->setUserId(null);
829
            $sessionEntity->setAsUserId(null);
830
            $saved = $sessionEntity->save();
831
            
832
            foreach ($sessionEntity->getMessages() as $message) {
833
                $validation->appendMessage($message);
834
            }
835
        }
836
        
837
        return [
838
            'saved' => $saved,
839
            'loggedIn' => $this->isLoggedIn(false, true),
840
            'loggedInAs' => $this->isLoggedIn(true, true),
841
            'messages' => $validation->getMessages(),
842
        ];
843
    }
844
    
845
    /**
846
     * @param array|null $params
847
     *
848
     * @return array
849
     */
850
    public function reset(array $params = null)
851
    {
852
        $saved = false;
853
        $sent = false;
854
        $token = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $token is dead and can be removed.
Loading history...
855
        
856
        $session = $this->getSession();
857
        $validation = new Validation();
858
        
859
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
860
        $validation->validate($params);
861
        
862
        if (!$session) {
863
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
864
        }
865
        else {
866
            $user = false;
867
            if (isset($params['email'])) {
868
                $userClass = $this->getUserClass();
869
                $user = $userClass::findFirstByEmail($params['email']);
870
            }
871
            
872
            // Reset
873
            if ($user) {
874
                
875
                // Password reset request
876
                if (empty($params['token'])) {
877
                    
878
                    // Generate a new token
879
                    $token = $user->prepareToken();
880
                    
881
                    // Send it by email
882
                    $emailClass = $this->getEmailClass();
883
                    
884
                    $email = new $emailClass();
885
                    $email->setViewPath('template/email');
886
                    $email->setTemplateByIndex('reset-password');
887
                    $email->setTo([$user->getEmail()]);
888
                    $meta = [];
889
                    $meta['user'] = $user->expose(['User' => [
890
                        false,
891
                        'firstName',
892
                        'lastName',
893
                        'email',
894
                    ]]);
895
                    $meta['resetLink'] = $this->url->get('/reset-password/' . $token);
896
                    
897
                    $email->setMeta($meta);
898
                    $saved = $user->save();
899
                    $sent = $saved ? $email->send() : false;
900
                    
901
                    // Appending error messages
902
                    foreach (['user', 'email'] as $e) {
903
                        foreach ($$e->getMessages() as $message) {
904
                            $validation->appendMessage($message);
905
                        }
906
                    }
907
                }
908
                
909
                // Password reset
910
                else {
911
                    $validation->add('password', new PresenceOf(['message' => 'password is required']));
912
                    $validation->add('passwordConfirm', new PresenceOf(['message' => 'password confirm is required']));
913
                    $validation->add('password', new Confirmation(['message' => 'password does not match passwordConfirm', 'with' => 'passwordConfirm']));
914
                    $validation->validate($params);
915
                    
916
                    if (!$user->checkToken($params['token'])) {
917
                        $validation->appendMessage(new Message('invalid token', 'token', 'NotValid', 400));
918
                    }
919
                    else {
920
                        if (!count($validation->getMessages())) {
921
                            $params['token'] = null;
922
                            $user->assign($params, ['token', 'password', 'passwordConfirm']);
923
                            $saved = $user->save();
924
                        }
925
                    }
926
                }
927
                
928
                // Appending error messages
929
                foreach ($user->getMessages() as $message) {
930
                    $validation->appendMessage($message);
931
                }
932
            }
933
            else {
934
//                $validation->appendMessage(new Message('User not found', 'user', 'PresenceOf', 404));
935
                // OWASP Protect User Enumeration
936
                $saved = true;
937
                $sent = true;
938
            }
939
        }
940
        
941
        return [
942
            'saved' => $saved,
943
            'sent' => $sent,
944
            'messages' => $validation->getMessages(),
945
        ];
946
    }
947
    
948
    /**
949
     * Get key / token fields to use for the session fetch & validation
950
     *
951
     * @return array
952
     */
953
    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

953
    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

953
    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...
954
    {
955
        $basicAuth = $this->request->getBasicAuth();
956
        $authorization = array_filter(explode(' ', $this->request->getHeader(
957
            $this->config->path('identity.authorizationHeader', 'Authorization')
958
        ) ?: ''));
959
        
960
        $jwt = $this->request->get('jwt', 'string', $jwt);
961
        $key = $this->request->get('key', 'string', $this->store['key'] ?? null);
962
        $token = $this->request->get('token', 'string', $this->store['token'] ?? null);
963
        
964
        if (!empty($key) && !empty($token)) {
965
        
966
        }
967
        
968
        else if (!empty($jwt)) {
969
            $sessionClaim = $this->getClaim($jwt, $this->sessionKey);
970
            $key = $sessionClaim->key ?? null;
971
            $token = $sessionClaim->token ?? null;
972
        }
973
        
974
        else if (!empty($basicAuth)) {
975
            $key = $basicAuth['username'] ?? null;
976
            $token = $basicAuth['password'] ?? null;
977
        }
978
        
979
        else if (!empty($authorization)) {
980
            $authorizationType = $authorization[0] ?? 'Bearer';
981
            $authorizationToken = $authorization[1] ?? null;
982
            
983
            if (strtolower($authorizationType) === 'bearer') {
984
                $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

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