Passed
Push — 1.0.x ( 8353d5...3a2c37 )
by Julien
21:28
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 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