Test Failed
Push — master ( 55df4f...a8146b )
by Julien
05:12
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 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 9
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
25
/**
26
 * Class Identity
27
 * {@inheritDoc}
28
 *
29
 * @author Julien Turbide <[email protected]>
30
 * @copyright Zemit Team <[email protected]>
31
 *
32
 * @since 1.0
33
 * @version 1.0
34
 *
35
 * @package Zemit
36
 */
37
class Identity extends Injectable
38
{
39
    /**
40
     * Without encryption
41
     */
42
    const MODE_DEFAULT = self::MODE_JWT;
43
    
44
    /**
45
     * Without encryption (raw string into the session)
46
     */
47
    const MODE_STRING = 'string';
48
    
49
    /**
50
     * Store using JWT (jwt encrypted into the session)
51
     */
52
    const MODE_JWT = 'jwt';
53
    
54
    /**
55
     * Locale mode for the prepare fonction
56
     * @var string
57
     */
58
    public string $mode = self::MODE_DEFAULT;
59
    
60
    /**
61
     * @var mixed|string|null
62
     */
63
    public $sessionKey = 'zemit-identity';
64
    
65
    /**
66
     * @var array
67
     */
68
    public $options = [];
69
    
70
    /**
71
     * @var User
72
     */
73
    public $user;
74
    
75
    /**
76
     * @var User
77
     */
78
    public $userAs;
79
    
80
    /**
81
     * @var Session
82
     */
83
    public $currentSession;
84
    
85
    /**
86
     * @var string|int|bool|null
87
     */
88
    public $identity;
89
    
90
    public function __construct($options = [])
91
    {
92
        $this->setOptions($options);
93
        $this->sessionKey = $this->getOption('sessionKey', $this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type string; however, parameter $default of Zemit\Identity::getOption() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

93
        $this->sessionKey = $this->getOption('sessionKey', /** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
94
        $this->setMode($this->getOption('mode', $this->mode));
95
//        $this->set($this->getFromSession());
96
    }
97
    
98
    /**
99
     * Set default options
100
     *
101
     * @param array $options
102
     */
103
    public function setOptions($options = [])
104
    {
105
        $this->options = $options;
106
    }
107
    
108
    /**
109
     * Getting an option value from the key, allowing to specify a default value
110
     *
111
     * @param $key
112
     * @param null $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
113
     *
114
     * @return mixed|null
115
     */
116
    public function getOption($key, $default = null)
117
    {
118
        return $this->options[$key] ?? $default;
119
    }
120
    
121
    /**
122
     * @return string
123
     */
124
    public function getSessionClass()
125
    {
126
        return $this->getOption('sessionClass') ?? \Zemit\Models\Session::class;
127
    }
128
    
129
    /**
130
     * @return string
131
     */
132
    public function getUserClass()
133
    {
134
        return $this->getOption('userClass') ?? \Zemit\Models\User::class;
135
    }
136
    
137
    /**
138
     * @return string
139
     */
140
    public function getGroupClass()
141
    {
142
        return $this->getOption('groupClass') ?? \Zemit\Models\Group::class;
143
    }
144
    
145
    /**
146
     * @return string
147
     */
148
    public function getRoleClass()
149
    {
150
        return $this->getOption('roleClass') ?? \Zemit\Models\Role::class;
151
    }
152
    
153
    /**
154
     * @return string
155
     */
156
    public function getTypeClass()
157
    {
158
        return $this->getOption('roleClass') ?? \Zemit\Models\Type::class;
159
    }
160
    
161
    /**
162
     * @return string
163
     */
164
    public function getEmailClass()
165
    {
166
        return $this->getOption('emailClass') ?? \Zemit\Models\Email::class;
167
    }
168
    
169
    /**
170
     * Get the current mode
171
     * @return string
172
     */
173
    public function getMode()
174
    {
175
        return $this->mode;
176
    }
177
    
178
    /**
179
     * Set the mode
180
     *
181
     * @param string $mode
182
     *
183
     * @throws \Exception Throw an exception if the mode is not supported
184
     */
185
    public function setMode($mode)
186
    {
187
        switch($mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
188
            case self::MODE_STRING:
189
            case self::MODE_JWT:
190
                $this->mode = $mode;
191
                break;
192
            default:
193
                throw new \Exception('Identity mode `' . $mode . '` is not supported.');
194
                break;
195
        }
196
    }
197
    
198
    /**
199
     * @return bool|mixed
200
     */
201
    public function getFromSession()
202
    {
203
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
0 ignored issues
show
Unused Code introduced by
The assignment to $ret is dead and can be removed.
Loading history...
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

203
        $ret = $this->session->has($this->sessionKey) ? $ret = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey) : null;
Loading history...
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

203
        $ret = $this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey) ? $ret = $this->session->get($this->sessionKey) : null;
Loading history...
204
        
205
        if ($ret) {
206
            switch($this->mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
207
                case self::MODE_DEFAULT:
208
                    break;
209
                case self::MODE_JWT:
210
                    $ret = $this->jwt->parseToken($ret)->getClaim('identity');
0 ignored issues
show
Bug introduced by
The method parseToken() does not exist on null. ( Ignorable by Annotation )

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

210
                    $ret = $this->jwt->/** @scrutinizer ignore-call */ parseToken($ret)->getClaim('identity');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property jwt does not exist on Zemit\Identity. Since you implemented __get, consider adding a @property annotation.
Loading history...
211
                    break;
212
            }
213
        }
214
        
215
        return json_decode($ret);
216
    }
217
    
218
    /**
219
     * Save an identity into the session
220
     *
221
     * @param int|string|null $identity
222
     */
223
    public function setIntoSession($identity)
224
    {
225
        
226
        $identity = json_encode($identity);
227
        
228
        $token = null;
229
        switch($this->mode) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
230
            case self::MODE_JWT:
231
                $token = $this->jwt->getToken(['identity' => $identity]);
0 ignored issues
show
Bug Best Practice introduced by
The property jwt does not exist on Zemit\Identity. Since you implemented __get, consider adding a @property annotation.
Loading history...
232
                break;
233
        }
234
        
235
        $this->session->set($this->sessionKey, $token ? : $identity);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

235
        $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $token ? : $identity);
Loading history...
236
    }
237
    
238
    /**
239
     * Set an identity
240
     *
241
     * @param int|string|null $identity
242
     */
243
    public function set($identity)
244
    {
245
        $this->setIntoSession($identity);
246
        $this->identity = $identity;
247
    }
248
    
249
    /**
250
     * Get the current identity
251
     * @return int|string|null
252
     */
253
    public function get()
254
    {
255
        $this->identity ??= $this->getFromSession();
256
        
257
        return $this->identity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->identity also could return the type boolean which is incompatible with the documented return type integer|null|string.
Loading history...
258
    }
259
    
260
    /**
261
     * @param null $roles
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $roles is correct as it would always require null to be passed?
Loading history...
262
     *
263
     * @return bool
264
     */
265
    public function hasRole($roles = null, $or = false)
266
    {
267
        return $this->has($roles, array_keys($this->getRoleList() ? : []), $or);
268
    }
269
    
270
    /**
271
     * Return the current user ID
272
     *
273
     * @return string|int|bool
274
     */
275
    public function getUserId($as = false)
276
    {
277
        /** @var User $user */
278
        $user = $this->getUser($as);
279
        
280
        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...
281
    }
282
    
283
    /**
284
     * Check if the needles meet the haystack using nested arrays
285
     * Reversing ANDs and ORs within each nested subarray
286
     *
287
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), true); // 'dev' OR 'admin'
288
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), false); // 'dev' ADN 'admin'
289
     *
290
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles()); // 'dev' AND 'admin'
291
     * $this->has([['dev', 'admin']], $this->getUser()->getRoles()); // 'dev' OR 'admin'
292
     * $this->has([[['dev', 'admin']]], $this->getUser()->getRoles()); // 'dev' AND 'admin'
293
     *
294
     * @param array|string|null $needles Needles to match and meet the rules
295
     * @param array $haystack Haystack array to search into
296
     * @param bool $or True to force with "OR" , false to force "AND" condition
297
     *
298
     * @return bool Return true or false if the needles rules are being met
299
     */
300
    public function has($needles = null, array $haystack = [], $or = false)
301
    {
302
        if (!is_array($needles)) {
303
            $needles = [$needles];
304
        }
305
        
306
        $result = [];
307
        foreach ([...$needles] as $needle) {
308
            if (is_array($needle)) {
309
                $result [] = $this->has($needle, $haystack, !$or);
310
            }
311
            else {
312
                $result [] = in_array($needle, $haystack, true);
313
            }
314
        }
315
        
316
        return $or ?
317
            !in_array(false, $result, true) :
318
            in_array(true, $result, true);
319
    }
320
    
321
    /**
322
     * Create a refresh a session
323
     *
324
     * @param bool $refresh
325
     *
326
     * @throws \Phalcon\Security\Exception
327
     */
328
    public function getJwt($refresh = false)
329
    {
330
        [$key, $token] = $this->getKeyToken();
331
        
332
        $key ??= $this->security->getRandom()->uuid();
333
        $token ??= $this->security->getRandom()->hex(512);
334
        
335
        $sessionClass = $this->getSessionClass();
336
        $session = $this->getSession($key, $token) ? : new $sessionClass();
337
        
338
        if ($session && $refresh) {
339
            $token = $this->security->getRandom()->hex(512);
340
        }
341
        
342
        $session->setKey($key);
343
        $session->setToken($session->hash($key . $token));
344
        $session->setDate(date('Y-m-d H:i:s'));
345
        $store = ['key' => $session->getKey(), 'token' => $token];
346
        
347
        ($save = $session->save()) ?
348
            $this->session->set($this->sessionKey, $store) :
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

348
            $this->session->set(/** @scrutinizer ignore-type */ $this->sessionKey, $store) :
Loading history...
349
            $this->session->remove($this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::remove() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

349
            $this->session->remove(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
350
        
351
        return [
352
            'saved' => $save,
353
            'stored' => $this->session->has($this->sessionKey),
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

946
                $sessionClaim = $this->getClaim(/** @scrutinizer ignore-type */ $authorizationToken, $this->sessionKey);
Loading history...
947
                $key = $sessionClaim->key ?? null;
948
                $token = $sessionClaim->token ?? null;
949
            }
950
        }
951
        
952
        else if ($this->session->has($this->sessionKey)) {
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

952
        else if ($this->session->has(/** @scrutinizer ignore-type */ $this->sessionKey)) {
Loading history...
953
            $sessionStore = $this->session->get($this->sessionKey);
0 ignored issues
show
Bug introduced by
It seems like $this->sessionKey can also be of type null; however, parameter $key of Phalcon\Session\ManagerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

953
            $sessionStore = $this->session->get(/** @scrutinizer ignore-type */ $this->sessionKey);
Loading history...
954
            $key = $sessionStore['key'] ?? null;
955
            $token = $sessionStore['token'] ?? null;
956
        }
957
        
958
        return [$key, $token];
959
    }
960
    
961
    /**
962
     * Return the session by key if the token is valid
963
     *
964
     * @param string $key
965
     * @param string $token
966
     * @param bool $refresh Pass true to force a session fetch from the database
967
     *
968
     * @return void|bool|Session Return the session by key if the token is valid, false otherwise
969
     */
970
    public function getSession(string $key = null, string $token = null, bool $refresh = false)
971
    {
972
        if (!isset($key, $token)) {
973
            [$key, $token] = $this->getKeyToken();
974
        }
975
        
976
        if (empty($key) || empty($token)) {
977
            return false;
978
        }
979
        
980
        if ($refresh) {
981
            $this->currentSession = null;
982
        }
983
        
984
        if (isset($this->currentSession)) {
985
            return $this->currentSession;
986
        }
987
        
988
        $sessionClass = $this->getSessionClass();
989
        $session = $sessionClass::findFirstByKey($this->filter->sanitize($key, 'string'));
990
        
991
        if ($session && $session->checkHash($session->getToken(), $key . $token)) {
992
            $this->currentSession = $session;
993
        }
994
        
995
        return $this->currentSession;
996
    }
997
    
998
    /**
999
     * Get a claim
1000
     * @TODO generate private & public keys
1001
     *
1002
     * @param string $token
1003
     * @param string|null $claim
1004
     *
1005
     * @return array|mixed
1006
     */
1007
    public function getClaim(string $token, string $claim = null)
1008
    {
1009
        $jwt = (new Parser())->parse((string)$token);
1010
        
1011
        return $claim ? $jwt->getClaim($claim) : $jwt->getClaims();
1012
    }
1013
    
1014
    /**
1015
     * Generate a new JWT
1016
     * @TODO generate private & public keys and use them
1017
     *
1018
     * @param $claim
1019
     * @param $data
1020
     *
1021
     * @return string
1022
     */
1023
    public function getJwtToken($claim, $data)
1024
    {
1025
        $uri = $this->request->getScheme() . '://' . $this->request->getHttpHost();
1026
1027
//        $privateKey = new Key('file://{path to your private key}');
1028
        $signer = new Sha512();
1029
        $time = time();
1030
        
1031
        $token = (new Builder())
1032
            ->issuedBy($uri) // Configures the issuer (iss claim)
1033
            ->permittedFor($uri) // Configures the audience (aud claim)
1034
            ->identifiedBy($claim, true) // Configures the id (jti claim), replicating as a header item
1035
            ->issuedAt($time) // Configures the time that the token was issue (iat claim)
1036
            ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
1037
            ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
1038
            ->withClaim($claim, $data) // Configures a new claim, called "uid"
1039
            ->getToken($signer) // Retrieves the generated token
1040
//            ->getToken($signer,  $privateKey); // Retrieves the generated token
1041
        ;
1042
        
1043
        return (string)$token;
1044
    }
1045
    
1046
    /**
1047
     * Retrieve the user from a username or an email
1048
     *
1049
     * @param $usernameOrEmail
1050
     *
1051
     * @return mixed
1052
     * @todo maybe move this into user model?
1053
     *
1054
     */
1055
    public function findUser($idUsernameEmail)
1056
    {
1057
        if (empty($idUsernameEmail)) {
1058
            return false;
1059
        }
1060
        
1061
        $userClass = $this->getUserClass();
1062
        
1063
        if (!is_int($idUsernameEmail)) {
1064
            $usernameEmail = $this->filter->sanitize($idUsernameEmail, ['string', 'trim']);
1065
            $user = $userClass::findFirst([
1066
                'email = :email: or username = :username:',
1067
                'bind' => [
1068
                    'email' => $usernameEmail,
1069
                    'username' => $usernameEmail,
1070
                ],
1071
                'bindTypes' => [
1072
                    'email' => Column::BIND_PARAM_STR,
1073
                    'username' => Column::BIND_PARAM_STR,
1074
                ],
1075
            ]);
1076
        }
1077
        else {
1078
            $user = $userClass::findFirstById((int)$idUsernameEmail);
1079
        }
1080
        
1081
        return $user;
1082
    }
1083
}
1084