Passed
Push — 1.0.x ( 8353d5...3a2c37 )
by Julien
21:28
created

Identity::getUser()   C

Complexity

Conditions 13
Paths 183

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 127.6982

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 13
eloc 30
c 1
b 1
f 0
nc 183
nop 2
dl 0
loc 48
ccs 4
cts 33
cp 0.1212
crap 127.6982
rs 5.925

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