Test Failed
Push — master ( 4d9b91...c71310 )
by Julien
06:31
created

Identity::getKeyToken()   B

Complexity

Conditions 9
Paths 14

Size

Total Lines 57
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 16.6047

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 9
eloc 31
c 4
b 1
f 0
nc 14
nop 3
dl 0
loc 57
ccs 18
cts 33
cp 0.5455
crap 16.6047
rs 8.0555

How to fix   Long Method   

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