Test Failed
Push — master ( 16c22a...3a800f )
by Julien
03:51
created

Identity::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
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
    public function initialize(): void
97
    {
98
        $this->sessionKey = $this->getOption('sessionKey', $this->sessionKey);
99
        $this->setMode($this->getOption('mode', $this->mode));
100
    }
101
    
102
    /**
103
     * Get the current mode
104
     * @return string
105
     */
106
    public function getMode(): string
107
    {
108
        return $this->mode;
109
    }
110
    
111
    /**
112
     * Set the mode
113
     *
114
     * @param string $mode
115
     *
116
     * @throws \Exception Throw an exception if the mode is not supported
117
     */
118
    public function setMode($mode)
119
    {
120
        switch ($mode) {
121
            case self::MODE_STRING:
122
            case self::MODE_JWT:
123
                $this->mode = $mode;
124
                break;
125
            default:
126
                throw new \Exception('Identity mode `' . $mode . '` is not supported.');
127
                break;
128
        }
129
    }
130
    
131
    /**
132
     * @return bool|mixed
133
     */
134
    public function getFromSession()
135
    {
136
        $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...
137
        
138
        if ($ret) {
139
            switch ($this->mode) {
140
                case self::MODE_DEFAULT:
141
                    break;
142
                case self::MODE_JWT:
143
                    $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

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

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

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

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

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

949
    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...
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

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

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