Test Failed
Push — master ( 301e07...8d9b0b )
by Julien
07:35
created

Identity::getIdentity()   D

Complexity

Conditions 22
Paths 45

Size

Total Lines 103
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 168.9423

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
eloc 60
c 1
b 0
f 0
nc 45
nop 1
dl 0
loc 103
ccs 20
cts 61
cp 0.3279
crap 168.9423
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit;
13
14
use Phalcon\Acl\Role;
15
use Phalcon\Db\Column;
16
use Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException;
17
use Phalcon\Filter\Validation\Validator\Confirmation;
18
use Phalcon\Filter\Validation\Validator\Numericality;
19
use Phalcon\Filter\Validation\Validator\PresenceOf;
20
use Phalcon\Messages\Message;
21
use Phalcon\Mvc\EntityInterface;
22
use Phalcon\Support\Helper\Str\Random;
23
use Zemit\Di\Injectable;
24
use Zemit\Filter\Validation;
25
use Zemit\Models\Interfaces\RoleInterface;
26
use Zemit\Models\Interfaces\SessionInterface;
27
use Zemit\Models\Interfaces\UserInterface;
28
use Zemit\Models\Oauth2;
29
use Zemit\Models\User;
30
use Zemit\Mvc\Model\Behavior\Security as SecurityBehavior;
31
use Zemit\Support\ModelsMap;
32
use Zemit\Support\Options\Options;
33
use Zemit\Support\Options\OptionsInterface;
34
35
/**
36
 * Identity Management
37
 */
38
class Identity extends Injectable implements OptionsInterface
39
{
40
    use Options;
41
    use ModelsMap;
42
    
43
    public string $sessionKey;
44
    
45
    public array $store = [];
46
    
47
    public ?UserInterface $user;
48
    
49
    public ?UserInterface $userAs;
50
    
51
    public ?SessionInterface $currentSession = null;
52
    
53
    /**
54
     * Forces some options
55
     */
56 6
    public function initialize(): void
57
    {
58 6
        $this->sessionKey = $this->getOption('sessionKey') ?? $this->sessionKey;
59 6
        $this->setModelsMap($this->getOption('modelsMap'));
60
    }
61
    
62
    /**
63
     * Check whether the current identity has roles
64
     */
65
    public function hasRole(?array $roles = null, bool $or = false, bool $inherit = true): bool
66
    {
67
        return $this->has($roles, array_keys($this->getRoleList($inherit) ?: []), $or);
68
    }
69
    
70
    /**
71
     * Get the User ID
72
     */
73 1
    public function getUserId(bool $as = false): ?int
74
    {
75 1
        $user = $this->getUser($as);
76 1
        return isset($user)? (int)$user->getId() : null;
77
    }
78
    
79
    /**
80
     * Get the User (As) ID
81
     */
82 1
    public function getUserAsId(): ?int
83
    {
84 1
        return $this->getUserId(true);
85
    }
86
    
87
    /**
88
     * Check if the needles meet the haystack using nested arrays
89
     * Reversing ANDs and ORs within each nested subarray
90
     *
91
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), true); // 'dev' OR 'admin'
92
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles(), false); // 'dev' AND 'admin'
93
     *
94
     * $this->has(['dev', 'admin'], $this->getUser()->getRoles()); // 'dev' AND 'admin'
95
     * $this->has([['dev', 'admin']], $this->getUser()->getRoles()); // 'dev' OR 'admin'
96
     * $this->has([[['dev', 'admin']]], $this->getUser()->getRoles()); // 'dev' AND 'admin'
97
     *
98
     * @param array|string|null $needles Needles to match and meet the rules
99
     * @param array $haystack Haystack array to search into
100
     * @param bool $or True to force with "OR" , false to force "AND" condition
101
     *
102
     * @return bool Return true or false if the needles rules are being met
103
     */
104
    public function has(array|string|null $needles = null, array $haystack = [], bool $or = false): bool
105
    {
106
        if (!is_array($needles)) {
107
            $needles = isset($needles)? [$needles] : [];
108
        }
109
        
110
        $result = [];
111
        foreach ($needles as $needle) {
112
            if (is_array($needle)) {
113
                $result [] = $this->has($needle, $haystack, !$or);
114
            } else {
115
                $result [] = in_array($needle, $haystack, true);
116
            }
117
        }
118
        
119
        return $or ?
120
            !in_array(false, $result, true) :
121
            in_array(true, $result, true);
122
    }
123
    
124
    /**
125
     * Create or refresh a session
126
     * @throws ValidatorException|\Phalcon\Encryption\Security\Exception
127
     */
128
    public function getJwt(bool $refresh = false): array
129
    {
130
        [$key, $token] = $this->getKeyToken();
131
        
132
        // generate new key & token pair if not set
133
        $key ??= $this->security->getRandom()->uuid();
134
        $token ??= $this->helper->random(Random::RANDOM_ALNUM, rand(111, 222));
135
        $newToken = $refresh ? $this->helper->random(Random::RANDOM_ALNUM, rand(111, 222)) : $token;
136
        
137
        // retrieve or create a new session
138
        $sessionClass = $this->getSessionClass();
139
        $session = $this->getSession($key, $token) ?? new $sessionClass();
140
        assert($session instanceof SessionInterface);
141
        
142
        // save the key token into the store (database or session)
143
        $session->setKey($key);
144
        $session->setToken($session->hash($key . $newToken));
145
        $session->setDate(date('Y-m-d H:i:s'));
146
        $saved = $session->save();
147
        
148
        // temporary store the new key token pair
149
        $this->store = ['key' => $session->getKey(), 'token' => $newToken];
150
        
151
        if ($saved && $this->config->path('identity.sessionFallback', false)) {
152
            // store key & token into the session
153
            $this->session->set($this->sessionKey, $this->store);
154
        }
155
        else {
156
            // delete the session
157
            $this->session->remove($this->sessionKey);
158
        }
159
        
160
        // jwt token
161
        $tokenOptions = $this->getConfig()->pathToArray('identity.token') ?? [];
162
        $token = $this->getJwtToken($this->sessionKey, $this->store, $tokenOptions);
163
        
164
        // refresh jwt token
165
        $refreshTokenOptions = $this->getConfig()->pathToArray('identity.refreshToken') ?? [];
166
        $refreshToken = $this->getJwtToken($this->sessionKey . '-refresh', $this->store, $refreshTokenOptions);
167
        
168
        return [
169
            'saved' => $saved,
170
            'hasSession' => $this->session->has($this->sessionKey),
171
            'refreshed' => $saved && $refresh,
172
            'validated' => $session->checkHash($session->getToken(), $session->getKey() . $newToken),
173
            'messages' => $session->getMessages(),
174
            'jwt' => $token,
175
            'refreshToken' => $refreshToken,
176
        ];
177
    }
178
    
179
    /**
180
     * Get basic Identity information
181
     * @throws \Exception
182
     */
183 1
    public function getIdentity(bool $inherit = true): array
184
    {
185 1
        $user = $this->getUser();
186 1
        $userAs = $this->getUserAs();
187
        
188 1
        $roleList = [];
189 1
        $groupList = [];
190 1
        $typeList = [];
191
        
192 1
        if (isset($user)) {
193
            if (!empty($user->rolelist)) {
194
                foreach ($user->rolelist as $role) {
195
                    $roleList [$role->getIndex()] = $role;
196
                }
197
            }
198
            
199
            if (!empty($user->grouplist)) {
200
                foreach ($user->grouplist as $group) {
201
                    $groupList [$group->getIndex()] = $group;
202
                    if (!empty($group->rolelist)) {
203
                        foreach ($group->rolelist as $role) {
204
                            $roleList [$role->getIndex()] = $role;
205
                        }
206
                    }
207
                }
208
            }
209
            
210
            if (!empty($user->typelist)) {
211
                foreach ($user->typelist as $type) {
212
                    $typeList [$type->getIndex()] = $type;
213
                    if (!empty($type->grouplist)) {
214
                        foreach ($type->grouplist as $group) {
215
                            $groupList [$group->getIndex()] = $group;
216
                            if (!empty($group->rolelist)) {
217
                                foreach ($group->rolelist as $role) {
218
                                    $roleList [$role->getIndex()] = $role;
219
                                }
220
                            }
221
                        }
222
                    }
223
                }
224
            }
225
        }
226
        
227
        // Append inherit roles
228 1
        if ($inherit) {
229 1
            $roleIndexList = [];
230 1
            foreach ($roleList as $role) {
231
                $roleIndexList [] = $role->getIndex();
232
            }
233
            
234 1
            $inheritedRoleIndexList = $this->getInheritedRoleList($roleIndexList);
235 1
            if (!empty($inheritedRoleIndexList)) {
236
                
237
                SecurityBehavior::staticStart();
238
                $roleClass = $this->getRoleClass();
239
                $inheritedRoleList = $this->models->getRole()::find([
240
                    'index in ({role:array})',
241
                    'bind' => ['role' => $inheritedRoleIndexList],
242
                    'bindTypes' => ['role' => Column::BIND_PARAM_STR],
243
                ]);
244
                SecurityBehavior::staticStop();
245
                
246
                assert(is_iterable($inheritedRoleList));
247
                foreach ($inheritedRoleList as $inheritedRoleEntity) {
248
                    assert($inheritedRoleEntity instanceof RoleInterface);
249
                    $inheritedRoleIndex = $inheritedRoleEntity->getIndex();
250
                    $roleList[$inheritedRoleIndex] = $inheritedRoleEntity;
251
                    
252
                    if (($key = array_search($inheritedRoleIndex, $inheritedRoleIndexList)) !== false) {
253
                        unset($inheritedRoleIndexList[$key]);
254
                    }
255
                }
256
                
257
                // unable to find some roles by index
258
                if (!empty($inheritedRoleIndexList)) {
259
                    
260
                    // To avoid breaking stuff in production, create a new role if it doesn't exist
261
                    if (!$this->config->path('app.debug', false)) {
262
                        foreach ($inheritedRoleIndexList as $inheritedRoleIndex) {
263
                            $roleList[$inheritedRoleIndex] = new $roleClass();
264
                            $roleList[$inheritedRoleIndex]->setIndex($inheritedRoleIndex);
265
                            $roleList[$inheritedRoleIndex]->setLabel(ucfirst($inheritedRoleIndex));
266
                        }
267
                    }
268
                    
269
                    // throw an exception under development so it can be fixed
270
                    else {
271
                        throw new \Exception('Role `' . implode('`, `', $inheritedRoleIndexList) . '` not found using the class `' . $this->getRoleClass() . '`.', 404);
272
                    }
273
                }
274
            }
275
        }
276
        
277
        // We don't need userAs group / type / role list
278 1
        return [
279 1
            'loggedIn' => $this->isLoggedIn(),
280 1
            'loggedInAs' => $this->isLoggedInAs(),
281 1
            'user' => $user,
282 1
            'userAs' => $userAs,
283 1
            'roleList' => $roleList,
284 1
            'typeList' => $typeList,
285 1
            'groupList' => $groupList,
286 1
        ];
287
    }
288
    
289
    /**
290
     * Return the list of inherited role list (recursively)
291
     */
292 1
    public function getInheritedRoleList(array $roleIndexList = []): array
293
    {
294 1
        $inheritedRoleList = [];
295 1
        $processedRoleIndexList = [];
296
        
297
        // While we still have role index list to process
298 1
        while (!empty($roleIndexList)) {
299
            
300
            // Process role index list
301
            foreach ($roleIndexList as $roleIndex) {
302
                // Get inherited roles from config service
303
                
304
                $configRoleList = $this->config->path('permissions.roles.' . $roleIndex . '.inherit', false);
305
                
306
                if ($configRoleList) {
307
                    
308
                    // Append inherited role to process list
309
                    $roleList = $configRoleList->toArray();
310
                    $roleIndexList = array_merge($roleIndexList, $roleList);
311
                    $inheritedRoleList = array_merge($inheritedRoleList, $roleList);
312
                }
313
                
314
                // Add role index to processed list
315
                $processedRoleIndexList [] = $roleIndex;
316
            }
317
            
318
            // Keep the unprocessed role index list
319
            $roleIndexList = array_filter(array_unique(array_diff($roleIndexList, $processedRoleIndexList)));
320
        }
321
        
322
        // Return the list of inherited role list (recursively)
323 1
        return array_values(array_filter(array_unique($inheritedRoleList)));
324
    }
325
    
326
    /**
327
     * Return true if the user is currently logged in
328
     */
329 1
    public function isLoggedIn(bool $as = false, bool $force = false): bool
330
    {
331 1
        return !!$this->getUser($as, $force);
332
    }
333
    
334
    /**
335
     * Return true if the user is currently logged in
336
     */
337 1
    public function isLoggedInAs(bool $force = false): bool
338
    {
339 1
        return $this->isLoggedIn(true, $force);
340
    }
341
    
342
    /**
343
     * Return the user object based on the session
344
     *
345
     * @param bool $as Flag to indicate whether to get the user as another user
346
     * @param bool|null $force Flag to indicate whether to force the retrieval of the user object
347
     * 
348
     * @return UserInterface|null The user object or null if session is not available
349
     */
350 3
    public function getUser(bool $as = false, ?bool $force = null): ?UserInterface
351
    {
352
        // session required to fetch user
353 3
        $session = $this->getSession();
354 3
        if (!$session) {
355 3
            return null;
356
        }
357
        
358
        $force = $force
359
            || ($as && empty($this->userAs))
360
            || (!$as && empty($this->user));
361
        
362
        if ($force) {
363
            
364
            $userId = $as
365
                ? $session->getAsUserId()
366
                : $session->getUserId();
367
            
368
            $user = null;
369
            if (!empty($userId)) {
370
                SecurityBehavior::staticStart();
371
                
372
                $user = $this->models->getUser()::findFirstWith([
373
                    'RoleList',
374
                    'GroupList.RoleList',
375
                    'TypeList.GroupList.RoleList',
376
                ], [
377
                    'id = :id:',
378
                    'bind' => ['id' => (int)$userId],
379
                    'bindTypes' => ['id' => Column::BIND_PARAM_INT]
380
                ]);
381
                if ($user) {
382
                    assert($user instanceof UserInterface);
383
                }
384
                
385
                SecurityBehavior::staticStop();
386
            }
387
            
388
            $as
389
                ? $this->setUserAs($user)
390
                : $this->setUser($user);
391
            
392
            return $user instanceof UserInterface? $user : null;
393
        }
394
        
395
        return $as
396
            ? $this->userAs
397
            : $this->user;
398
    }
399
    
400
    /**
401
     * Get Identity User (As)
402
     */
403 1
    public function getUserAs(): ?UserInterface
404
    {
405 1
        return $this->getUser(true);
406
    }
407
    
408
    /**
409
     * Set Identity User
410
     */
411
    public function setUser(?UserInterface $user): void
412
    {
413
        $this->user = $user;
414
    }
415
    
416
    /**
417
     * Set Identity User (As)
418
     */
419
    public function setUserAs(?UserInterface $user): void
420
    {
421
        $this->userAs = $user;
422
    }
423
    
424
    /**
425
     * Get the "Roles" related to the current session
426
     */
427 1
    public function getRoleList(bool $inherit = true): array
428
    {
429 1
        return $this->getIdentity($inherit)['roleList'] ?? [];
430
    }
431
    
432
    /**
433
     * Get the "Groups" related to the current session
434
     */
435
    public function getGroupList(bool $inherit = true): array
436
    {
437
        return $this->getIdentity($inherit)['groupList'] ?? [];
438
    }
439
    
440
    /**
441
     * Get the "Types" related to the current session
442
     */
443
    public function getTypeList(bool $inherit = true): array
444
    {
445
        return $this->getIdentity($inherit)['typeList'] ?? [];
446
    }
447
    
448
    /**
449
     * Return the list of ACL roles
450
     * - Reserved roles: guest, cli, everyone
451
     *
452
     * @param array|null $roleList
453
     * @return array
454
     */
455 1
    public function getAclRoles(?array $roleList = null): array
456
    {
457 1
        $roleList ??= $this->getRoleList();
458 1
        $aclRoles = [];
459
        
460
        // Add everyone role
461 1
        $aclRoles['everyone'] = new Role('everyone', 'Everyone');
462
        
463
        // Add guest role if no roles was detected
464 1
        if (count($roleList) === 0) {
465 1
            $aclRoles['guest'] = new Role('guest', 'Guest');
466
        }
467
        
468
        // Add roles from databases
469 1
        foreach ($roleList as $role) {
470
            if ($role) {
471
                $aclRoles[$role->getIndex()] ??= new Role($role->getIndex());
472
            }
473
        }
474
        
475
        // Add console role
476 1
        if ($this->bootstrap->isCli()) {
477
            $aclRoles['cli'] = new Role('cli', 'Cli');
478
        }
479
        
480 1
        return array_filter(array_values(array_unique($aclRoles)));
481
    }
482
    
483
    /**
484
     * Login as User
485
     */
486
    public function loginAs(?array $params = []): array
487
    {
488
        $session = $this->getSession();
489
        
490
        // Validation
491
        $validation = new Validation();
492
        $validation->add('userId', new PresenceOf(['message' => 'required']));
493
        $validation->add('userId', new Numericality(['message' => 'not-numeric']));
494
        $validation->validate($params);
495
        $messages = $validation->getMessages();
496
    
497
        $saved = false;
498
        
499
        // must be an admin
500
        if (isset($session, $params['userId']) && !$messages->count() && $this->hasRole(['admin', 'dev'])) {
501
            $userId = $session->getUserId();
502
            
503
            // himself, return back to normal login
504
            if ((int)$params['userId'] === (int)$userId) {
505
                return $this->logoutAs();
506
            }
507
    
508
            // login as using id
509
            $asUser = $this->findUserById((int)$params['userId']);
510
            if (isset($asUser)) {
511
                $session->setAsUserId($userId);
512
                $session->setUserId($params['userId']);
513
            }
514
            else {
515
                $validation->appendMessage(new Message('User Not Found', 'userId', 'PresenceOf', 404));
516
            }
517
    
518
            $saved = $session->save();
519
            foreach ($session->getMessages() as $message) {
520
                $validation->appendMessage($message);
521
            }
522
        }
523
        
524
        return [
525
            'saved' => $saved,
526
            'messages' => $validation->getMessages(),
527
            'loggedIn' => $this->isLoggedIn(false, true),
528
            'loggedInAs' => $this->isLoggedIn(true, true),
529
        ];
530
    }
531
    
532
    /**
533
     * Log off User (As)
534
     */
535
    public function logoutAs(): array
536
    {
537
        $session = $this->getSession();
538
        
539
        if ($session) {
540
            $asUserId = $session->getAsUserId();
541
            $userId = $session->getUserId();
542
            if (!empty($asUserId) && !empty($userId)) {
543
                $session->setUserId($asUserId);
544
                $session->setAsUserId(null);
545
            }
546
        }
547
        
548
        return [
549
            'saved' => $session && $session->save(),
550
            'messages' => $session && $session->getMessages(),
551
            'loggedIn' => $this->isLoggedIn(false, true),
552
            'loggedInAs' => $this->isLoggedIn(true, true),
553
        ];
554
    }
555
    
556
    /**
557
     * OAuth2 authentication
558
     *
559
     * @param string $provider The OAuth2 provider
560
     * @param int $providerUuid The UUID associated with the provider
561
     * @param string $accessToken The access token provided by the provider
562
     * @param string|null $refreshToken The refresh token provided by the provider (optional)
563
     * @param array|null $meta Additional metadata associated with the user (optional)
564
     *
565
     * @return array Returns an array with the following keys:
566
     *   - 'saved': Indicates whether the OAuth2 entity was saved successfully
567
     *   - 'loggedIn': Indicates whether the user is currently logged in
568
     *   - 'loggedInAs': Indicates the user that is currently logged in
569
     *   - 'messages': An array of validation messages
570
     * 
571
     * @throws \Phalcon\Filter\Exception
572
     */
573
    public function oauth2(string $provider, int $providerUuid, string $accessToken, ?string $refreshToken = null, ?array $meta = []): array
574
    {
575
        $loggedInUser = null;
576
        $saved = false;
577
        
578
        // get the current session
579
        $session = $this->getSession();
580
        
581
        // prepare validation
582
        $validation = new Validation();
583
        
584
        // a session is required
585
        if (!isset($session)) {
586
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
587
        }
588
        else {
589
            // retrieve and prepare oauth2 entity
590
            $oauth2 = Oauth2::findFirst([
591
                'provider = :provider: and provider_uuid = :providerUuid:',
592
                'bind' => [
593
                    'provider' => $this->filter->sanitize($provider, 'string'),
594
                    'providerUuid' => $providerUuid,
595
                ],
596
                'bindTypes' => [
597
                    'provider' => Column::BIND_PARAM_STR,
598
                    'id' => Column::BIND_PARAM_STR,
599
                ],
600
            ]);
601
            if (!$oauth2) {
602
                $oauth2 = new Oauth2();
603
                $oauth2->setProvider($provider);
604
                $oauth2->setProviderUuid($providerUuid);
605
            }
606
            $oauth2->setAccessToken($accessToken);
607
            $oauth2->setRefreshToken($refreshToken);
608
            $oauth2->setMeta(!empty($meta)? json_encode($meta) : null);
609
            $oauth2->setName($meta['name'] ?? null);
610
            $oauth2->setFirstName($meta['first_name'] ?? null);
611
            $oauth2->setLastName($meta['last_name'] ?? null);
612
            $oauth2->setEmail($meta['email'] ?? null);
613
            
614
            // link the current user to the oauth2 entity
615
            $oauth2UserId = $oauth2->getUserId();
616
            $sessionUserId = $session->getUserId();
617
            if (empty($oauth2UserId) && !empty($sessionUserId)) {
618
                $oauth2->setUserId($sessionUserId);
619
            }
620
            
621
            // save the oauth2 entity
622
            $saved = $oauth2->save();
623
            
624
            // append oauth2 error messages
625
            foreach ($oauth2->getMessages() as $message) {
626
                $validation->appendMessage($message);
627
            }
628
            
629
            // user id is required
630
            $validation->add('userId', new PresenceOf(['message' => 'userId is required']));
631
            $validation->validate($oauth2->toArray());
632
            
633
            // All validation passed
634
            if ($saved && !$validation->getMessages()->count()) {
635
                $user = $this->findUserById($oauth2->getUserId());
636
                
637
                // user not found, login failed
638
                if (!isset($user)) {
639
                    $validation->appendMessage(new Message('Login Failed', ['id'], 'LoginFailed', 401));
640
                }
641
                
642
                // access forbidden, login failed
643
                elseif ($user->isDeleted()) {
644
                    $validation->appendMessage(new Message('Login Forbidden', 'password', 'LoginForbidden', 403));
645
                }
646
                
647
                // login success
648
                else {
649
                    $loggedInUser = $user;
650
                }
651
                
652
                // Set the oauth user id into the session
653
                $session->setUserId($loggedInUser?->getId());
654
                $saved = $session->save();
655
                
656
                // append session error messages
657
                foreach ($session->getMessages() as $message) {
658
                    $validation->appendMessage($message);
659
                }
660
            }
661
        }
662
        
663
        return [
664
            'saved' => $saved,
665
            'loggedIn' => $this->isLoggedIn(false, true),
666
            'loggedInAs' => $this->isLoggedIn(true, true),
667
            'messages' => $validation->getMessages(),
668
        ];
669
    }
670
    
671
    /**
672
     * Login request
673
     * Requires an active session to bind the logged in userId
674
     */
675
    public function login(array $params = null): array
676
    {
677
        $loggedInUser = null;
678
        $saved = null;
679
        $session = $this->getSession();
680
        $validation = new Validation();
681
        $validation->add('email', new PresenceOf(['message' => 'required']));
682
        $validation->add('password', new PresenceOf(['message' => 'required']));
683
        $validation->validate($params);
684
        
685
        if (!$session) {
686
            $validation->appendMessage(new Message('required', 'Session Required', 'PresenceOf', 403));
687
        }
688
        
689
        $messages = $validation->getMessages();
690
        if (isset($session, $params['email'], $params['password']) && !$messages->count()) {
691
            $user = empty($params['email'])? null : $this->findUser($params['email']);
692
            
693
            $loginFailedMessage = new Message('Login Failed', ['email', 'password'], 'LoginFailed', 401);
694
            $loginForbiddenMessage = new Message('Login Forbidden', ['email', 'password'], 'LoginForbidden', 403);
695
            
696
            if (!isset($user)) {
697
                // user not found, login failed
698
                $validation->appendMessage($loginFailedMessage);
699
            }
700
            elseif (empty($user->getPassword())) {
701
                // password disabled, login failed
702
                $validation->appendMessage($loginFailedMessage);
703
            }
704
            elseif (!$user->checkHash($user->getPassword(), $params['password'])) {
705
                // password failed, login failed
706
                $validation->appendMessage($loginFailedMessage);
707
            }
708
            elseif ($user->isDeleted()) {
709
                // password match, user is deleted login forbidden
710
                $validation->appendMessage($loginForbiddenMessage);
711
            }
712
            
713
            // login success
714
            else {
715
                $loggedInUser = $user;
716
            }
717
            
718
            $session->setUserId($loggedInUser?->getId());
719
            $saved = $session->save();
720
            foreach ($session->getMessages() as $message) {
721
                $validation->appendMessage($message);
722
            }
723
        }
724
        
725
        return [
726
            'saved' => $saved,
727
            'loggedIn' => $this->isLoggedIn(false, true),
728
            'loggedInAs' => $this->isLoggedIn(true, true),
729
            'messages' => $validation->getMessages(),
730
        ];
731
    }
732
    
733
    /**
734
     * Logout the user and return the logout status, login status, and validation messages
735
     * 
736
     * @return array The array containing the logout status, login status, and validation messages
737
     */
738
    public function logout(): array
739
    {
740
        $saved = false;
741
        $sessionEntity = $this->getSession();
742
        $validation = new Validation();
743
        $validation->validate();
744
        if (!$sessionEntity) {
745
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
746
        }
747
        else {
748
            // Logout
749
            $sessionEntity->setUserId(null);
750
            $sessionEntity->setAsUserId(null);
751
            $saved = $sessionEntity->save();
752
            foreach ($sessionEntity->getMessages() as $message) {
753
                $validation->appendMessage($message);
754
            }
755
        }
756
        
757
        return [
758
            'saved' => $saved,
759
            'loggedIn' => $this->isLoggedIn(false, true),
760
            'loggedInAs' => $this->isLoggedIn(true, true),
761
            'messages' => $validation->getMessages(),
762
        ];
763
    }
764
    
765
    /**
766
     * @param array|null $params
767
     *
768
     * @return array
769
     */
770
    public function reset(array $params = null)
771
    {
772
        $saved = false;
773
        $sent = false;
774
        $session = $this->getSession();
775
        $validation = new Validation();
776
        $validation->add('email', new PresenceOf(['message' => 'email is required']));
777
        $validation->validate($params);
778
        if (!$session) {
779
            $validation->appendMessage(new Message('A session is required', 'session', 'PresenceOf', 403));
780
        }
781
        else {
782
            $user = false;
783
            if (isset($params['email'])) {
784
                $user = $this->models->getUser()::findFirst([
785
                    'email = :email:',
786
                    'bind' => ['email' => $params['email']],
787
                    'bindTypes' => ['email', Column::BIND_PARAM_STR]
788
                ]);
789
            }
790
            
791
            // Reset
792
            if ($user) {
793
                // Password reset request
794
                if (empty($params['token'])) {
795
                    
796
                    // Generate a new token
797
                    $token = $user->prepareToken();
798
                    
799
                    // Send it by email
800
                    $emailClass = $this->getEmailClass();
801
                    $email = new $emailClass();
802
                    $email->setViewPath('template/email');
803
                    $email->setTemplateByIndex('reset-password');
804
                    $email->setTo([$user->getEmail()]);
805
                    $meta = [];
806
                    $meta['user'] = $user->expose(['User' => [
807
                        false,
808
                        'firstName',
809
                        'lastName',
810
                        'email',
811
                    ]]);
812
                    $meta['resetLink'] = $this->url->get('/reset-password/' . $token);
813
                    $email->setMeta($meta);
814
                    $saved = $user->save();
815
                    $sent = $saved ? $email->send() : false;
816
                    
817
                    // Appending error messages
818
                    foreach (['user', 'email'] as $e) {
819
                        foreach ($$e->getMessages() as $message) {
820
                            $validation->appendMessage($message);
821
                        }
822
                    }
823
                }
824
                
825
                // Password reset
826
                else {
827
                    $validation->add('password', new PresenceOf(['message' => 'password is required']));
828
                    $validation->add('passwordConfirm', new PresenceOf(['message' => 'password confirm is required']));
829
                    $validation->add('password', new Confirmation(['message' => 'password does not match passwordConfirm', 'with' => 'passwordConfirm']));
830
                    $validation->validate($params);
831
                    if (!$user->checkToken($params['token'])) {
832
                        $validation->appendMessage(new Message('invalid token', 'token', 'NotValid', 400));
833
                    }
834
                    elseif (!$validation->getMessages()->count()) {
835
                        $params['token'] = null;
836
                        $user->assign($params, ['token', 'password', 'passwordConfirm']);
837
                        $saved = $user->save();
838
                    }
839
                }
840
                
841
                // Appending error messages
842
                foreach ($user->getMessages() as $message) {
843
                    $validation->appendMessage($message);
844
                }
845
            }
846
            else {
847
                // removed - OWASP Protect User Enumeration
848
//                $validation->appendMessage(new Message('User not found', 'user', 'PresenceOf', 404));
849
                $saved = true;
850
                $sent = true;
851
            }
852
        }
853
        
854
        return [
855
            'saved' => $saved,
856
            'sent' => $sent,
857
            'messages' => $validation->getMessages(),
858
        ];
859
    }
860
    
861
    /**
862
     * Retrieve the key and token from various authorization sources
863
     *
864
     * @param string|null $jwt The JWT token
865
     * @param string|null $key The key
866
     * @param string|null $token The token
867
     * @return array An array containing the key and token
868
     * @throws ValidatorException
869
     */
870 3
    public function getKeyToken(string $jwt = null, string $key = null, string $token = null): array
871
    {
872 3
        $json = $this->request->getJsonRawBody();
873 3
        $refreshToken = $this->request->get('refreshToken', 'string', $json->refreshToken ?? null);
874 3
        $jwt ??= $this->request->get('jwt', 'string', $json->jwt ?? null);
875 3
        $key ??= $this->request->get('key', 'string', $this->store['key'] ?? $json->key ?? null);
876 3
        $token ??= $this->request->get('token', 'string', $this->store['token'] ?? $json->token ?? null);
877
        
878
        // Using provided key & token
879 3
        if (isset($key, $token)) {
880
            return [$key, $token];
881
        }
882
        
883
        // Using refresh token
884 3
        if (!empty($refreshToken)) {
885
            return $this->getKeyTokenFromClaimToken($refreshToken, $this->sessionKey . '-refresh');
886
        }
887
        
888
        // Using JWT
889 3
        if (!empty($jwt)) {
890
            return $this->getKeyTokenFromClaimToken($jwt, $this->sessionKey);
891
        }
892
        
893
        // Using Basic Auth from HTTP request
894 3
        $basicAuth = $this->request->getBasicAuth();
895 3
        if (!empty($basicAuth)) {
896
            return [
897
                $basicAuth['username'] ?? null,
898
                $basicAuth['password'] ?? null,
899
            ];
900
        }
901
        
902
        // Using X-Authorization Header
903 3
        $authorizationHeaderKey = $this->config->path('identity.authorizationHeader', 'Authorization');
904 3
        $authorizationHeaderValue = $this->request->getHeader($authorizationHeaderKey);
905 3
        $authorization = array_filter(explode(' ', $authorizationHeaderValue));
906 3
        if (!empty($authorization)) {
907
            return $this->getKeyTokenFromAuthorization($authorization);
908
        }
909
        
910
        // Using Session Fallback
911 3
        $sessionFallback = $this->config->path('identity.sessionFallback', false);
912 3
        if ($sessionFallback && $this->session->has($this->sessionKey)) {
913
            $sessionStore = $this->session->get($this->sessionKey);
914
            return [
915
                $sessionStore['key'] ?? null,
916
                $sessionStore['token'] ?? null,
917
            ];
918
        }
919
        
920
        // Unsupported authorization method
921 3
        return [null, null];
922
    }
923
    
924
    /**
925
     * Get key and token from authorization
926
     * @param array $authorization The authorization array, where the first element is the authorization type and the second element is the authorization token
927
     * @return array The key and token extracted from the authorization session claim. If the key or token is not found, null will be returned for that value.
928
     * @throws ValidatorException
929
     */
930
    public function getKeyTokenFromAuthorization(array $authorization): array
931
    {
932
        $authorizationType = $authorization[0] ?? null;
933
        $authorizationToken = $authorization[1] ?? null;
934
        
935
        if ($authorizationType && $authorizationToken && strtolower($authorizationType) === 'bearer') {
936
            return $this->getKeyTokenFromClaimToken($authorizationToken, $this->sessionKey);
937
        }
938
        
939
        return [null, null];
940
    }
941
    
942
    /**
943
     * Get the key and token from the claim token
944
     *
945
     * @param string $claimToken The claim token
946
     * @param string $sessionKey The session key
947
     * @return array The key and token, [key, token]
948
     * @throws ValidatorException
949
     */
950
    public function getKeyTokenFromClaimToken(string $claimToken, string $sessionKey): array
951
    {
952
        $sessionClaim = $this->getClaim($claimToken, $sessionKey);
953
        $key = $sessionClaim['key'] ?? null;
954
        $token = $sessionClaim['token'] ?? null;
955
        return [$key, $token];
956
    }
957
    
958
    /**
959
     * Return the session by key if the token is valid
960
     */
961 3
    public function getSession(?string $key = null, ?string $token = null, bool $refresh = false): ?SessionInterface
962
    {
963 3
        if (!isset($key, $token)) {
964 3
            [$key, $token] = $this->getKeyToken();
965
        }
966
        
967 3
        if (empty($key) || empty($token)) {
968 3
            return null;
969
        }
970
        
971
        if ($refresh) {
972
            $this->currentSession = null;
973
        }
974
        
975
        if (isset($this->currentSession)) {
976
            return $this->currentSession;
977
        }
978
        
979
        $sessionClass = $this->getSessionClass();
980
        $sessionEntity = $sessionClass::findFirstByKey($this->filter->sanitize($key, 'string'));
981
        if ($sessionEntity && $sessionEntity->checkHash($sessionEntity->getToken(), $key . $token)) {
982
            $this->currentSession = $sessionEntity;
983
        }
984
        
985
        return $this->currentSession;
986
    }
987
    
988
    /**
989
     * Return the session ID if available, otherwise return null
990
     *
991
     * @return int|null
992
     */
993
    public function getSessionId(): ?int
994
    {
995
        $session = $this->getSession();
996
        return $session instanceof EntityInterface? $session->readAttribute('id') : null;
997
    }
998
    
999
    /**
1000
     * @param string $token
1001
     * @param string|null $claim
1002
     * @return array
1003
     * @throws ValidatorException
1004
     */
1005
    public function getClaim(string $token, string $claim = null): array
1006
    {
1007
        $uri = $this->request->getScheme() . '://' . $this->request->getHttpHost();
1008
        
1009
        $token = $this->jwt->parseToken($token);
1010
        
1011
        $this->jwt->validateToken($token, 0, [
1012
            'issuer' => $uri,
1013
            'audience' => $uri,
1014
            'id' => $claim,
1015
        ]);
1016
        $claims = $token->getClaims();
1017
        
1018
        $ret = $claims->has('sub') ? json_decode($claims->get('sub'), true) : [];
1019
        return is_array($ret) ? $ret : [];
1020
    }
1021
    
1022
    /**
1023
     * Generate a new JWT Token (string)
1024
     * @throws ValidatorException
1025
     */
1026
    public function getJwtToken(string $id, array $data = [], array $options = []): string
1027
    {
1028
        $uri = $this->request->getScheme() . '://' . $this->request->getHttpHost();
1029
        
1030
        $options['issuer'] ??= $uri;
1031
        $options['audience'] ??= $uri;
1032
        $options['id'] ??= $id;
1033
        $options['subject'] ??= json_encode($data);
1034
        
1035
        $builder = $this->jwt->builder($options);
1036
        return $builder->getToken()->getToken();
1037
    }
1038
    
1039
    /**
1040
     * Get the User from the database using the ID
1041
     */
1042
    public function findUserById(int $id): ?UserInterface
1043
    {
1044
        $user = $this->models->getUser()::findFirst([
1045
            'id = :id:',
1046
            'bind' => ['id' => $id],
1047
            'bindTypes' => ['id' => Column::BIND_PARAM_INT],
1048
        ]);
1049
        return $user instanceof UserInterface? $user : null;
1050
    }
1051
    
1052
    /**
1053
     * Get the user from the database using the username or email
1054
     */
1055
    public function findUser(string $string): ?UserInterface
1056
    {
1057
        $user = $this->models->getUser()::findFirst([
1058
            'email = :email:',
1059
            'bind' => [
1060
                'email' => $string,
1061
            ],
1062
            'bindTypes' => [
1063
                'email' => Column::BIND_PARAM_STR,
1064
            ],
1065
        ]);
1066
        return $user instanceof UserInterface? $user : null;
1067
    }
1068
}
1069