AuthTrait   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 115
c 1
b 0
f 0
dl 0
loc 360
rs 9.2
wmc 40

15 Methods

Rating   Name   Duplication   Size   Complexity  
A twoStepVerification() 0 31 1
A isTwoFactorEnabled() 0 3 1
A isActivated() 0 3 1
A sendMail() 0 11 5
B verifySchema() 0 15 7
A check() 0 3 1
A verifyAndUpdateOtp() 0 23 4
A getVisibleFields() 0 13 4
A reset() 0 15 3
A forget() 0 27 2
A generateToken() 0 3 2
A getUser() 0 17 4
A signup() 0 24 2
A resendOtp() 0 9 2
A activate() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like AuthTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.7
13
 */
14
15
namespace Quantum\Libraries\Auth\Traits;
16
17
use Quantum\Libraries\Auth\Contracts\AuthServiceInterface;
18
use Quantum\Libraries\Mailer\Contracts\MailerInterface;
19
use Quantum\Libraries\Auth\Exceptions\AuthException;
20
use Quantum\Libraries\Jwt\Exceptions\JwtException;
21
use Quantum\Config\Exceptions\ConfigException;
22
use Quantum\Libraries\Auth\Constants\AuthKeys;
23
use Quantum\App\Exceptions\BaseException;
24
use Quantum\Di\Exceptions\DiException;
25
use Quantum\Libraries\Hasher\Hasher;
26
use Quantum\Libraries\Jwt\JwtToken;
27
use Quantum\Libraries\Auth\User;
28
use ReflectionException;
29
use ReflectionClass;
30
use DateInterval;
31
use Exception;
32
use DateTime;
33
34
/**
35
 * Trait AuthTrait
36
 * @package Quantum\Libraries\Auth
37
 */
38
trait AuthTrait
39
{
40
41
    /**
42
     * @var MailerInterface
43
     */
44
    protected $mailer;
45
46
    /**
47
     * @var Hasher
48
     */
49
    protected $hasher;
50
51
    /**
52
     * @var JwtToken
53
     */
54
    protected $jwt;
55
56
    /**
57
     * @var AuthServiceInterface
58
     */
59
    protected $authService;
60
61
    /**
62
     * @var int
63
     */
64
    protected $otpLength = 6;
65
66
    /**
67
     * @var array
68
     */
69
    protected $keyFields = [];
70
71
    /**
72
     * @var array
73
     */
74
    protected $visibleFields = [];
75
76
    /**
77
     * @inheritDoc
78
     * @throws ConfigException
79
     * @throws DiException
80
     * @throws JwtException
81
     * @throws ReflectionException
82
     * @throws BaseException
83
     */
84
    public function check(): bool
85
    {
86
        return !is_null($this->user());
0 ignored issues
show
Bug introduced by
It seems like user() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

86
        return !is_null($this->/** @scrutinizer ignore-call */ user());
Loading history...
87
    }
88
89
    /**
90
     * Sign Up
91
     * @param array $userData
92
     * @param array|null $customData
93
     * @return User
94
     */
95
    public function signup(array $userData, array $customData = null): User
96
    {
97
        $activationToken = $this->generateToken();
98
99
        $userData[$this->keyFields[AuthKeys::PASSWORD]] = $this->hasher->hash($userData[$this->keyFields[AuthKeys::PASSWORD]]);
100
        $userData[$this->keyFields[AuthKeys::ACTIVATION_TOKEN]] = $activationToken;
101
102
        $user = $this->authService->add($userData);
103
104
        $body = [
105
            'user' => $user,
106
            'activationToken' => $activationToken
107
        ];
108
109
        if ($customData) {
110
            $body = array_merge($body, $customData);
111
        }
112
113
        $this->mailer->setSubject(t('common.activate_account'));
114
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'activate');
115
116
        $this->sendMail($user, $body);
117
118
        return $user;
119
    }
120
121
    /**
122
     * Activate
123
     * @param string $token
124
     */
125
    public function activate(string $token)
126
    {
127
        $this->authService->update(
128
            $this->keyFields[AuthKeys::ACTIVATION_TOKEN],
129
            $token,
130
            [$this->keyFields[AuthKeys::ACTIVATION_TOKEN] => '']
131
        );
132
    }
133
134
    /**
135
     * Forget
136
     * @param string $username
137
     * @return string|null
138
     */
139
    public function forget(string $username): ?string
140
    {
141
        $user = $this->authService->get($this->keyFields[AuthKeys::USERNAME], $username);
142
143
        if (!$user) {
144
            return null;
145
        }
146
147
        $resetToken = $this->generateToken();
148
149
        $this->authService->update(
150
            $this->keyFields[AuthKeys::USERNAME],
151
            $username,
152
            [$this->keyFields[AuthKeys::RESET_TOKEN] => $resetToken]
153
        );
154
155
        $body = [
156
            'user' => $user,
157
            'resetToken' => $resetToken
158
        ];
159
160
        $this->mailer->setSubject(t('common.reset_password'));
161
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'reset');
162
163
        $this->sendMail($user, $body);
164
165
        return $resetToken;
166
    }
167
168
    /**
169
     * Reset
170
     * @param string $token
171
     * @param string $password
172
     */
173
    public function reset(string $token, string $password)
174
    {
175
        $user = $this->authService->get($this->keyFields[AuthKeys::RESET_TOKEN], $token);
176
177
        if ($user) {
178
            if (!$this->isActivated($user)) {
179
                $this->activate($token);
180
            }
181
182
            $this->authService->update(
183
                $this->keyFields[AuthKeys::RESET_TOKEN],
184
                $token,
185
                [
186
                    $this->keyFields[AuthKeys::PASSWORD] => $this->hasher->hash($password),
187
                    $this->keyFields[AuthKeys::RESET_TOKEN] => ''
188
                ]
189
            );
190
        }
191
    }
192
193
    /**
194
     * Resend OTP
195
     * @param string $otpToken
196
     * @return string
197
     * @throws AuthException
198
     * @throws Exception
199
     */
200
    public function resendOtp(string $otpToken): string
201
    {
202
        $user = $this->authService->get($this->keyFields[AuthKeys::OTP_TOKEN], $otpToken);
203
204
        if (!$user) {
205
            throw AuthException::incorrectCredentials();
206
        }
207
208
        return $this->twoStepVerification($user);
209
210
    }
211
212
    /**
213
     * Gets the user by username and password
214
     * @param string $username
215
     * @param string $password
216
     * @return User
217
     * @throws AuthException
218
     */
219
    protected function getUser(string $username, string $password): User
220
    {
221
        $user = $this->authService->get($this->keyFields[AuthKeys::USERNAME], $username);
222
223
        if (!$user) {
224
            throw AuthException::incorrectCredentials();
225
        }
226
227
        if (!$this->hasher->check($password, $user->getFieldValue($this->keyFields[AuthKeys::PASSWORD]))) {
0 ignored issues
show
Bug introduced by
It seems like $user->getFieldValue($th...ts\AuthKeys::PASSWORD]) can also be of type null; however, parameter $hash of Quantum\Libraries\Hasher\Hasher::check() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

227
        if (!$this->hasher->check($password, /** @scrutinizer ignore-type */ $user->getFieldValue($this->keyFields[AuthKeys::PASSWORD]))) {
Loading history...
228
            throw AuthException::incorrectCredentials();
229
        }
230
231
        if (!$this->isActivated($user)) {
232
            throw AuthException::inactiveAccount();
233
        }
234
235
        return $user;
236
    }
237
238
    /**
239
     * Two-Step Verification
240
     * @param User $user
241
     * @return string
242
     * @throws Exception
243
     */
244
    protected function twoStepVerification(User $user): string
245
    {
246
        $otp = random_number($this->otpLength);
247
248
        $otpToken = $this->generateToken($user->getFieldValue($this->keyFields[AuthKeys::USERNAME]));
249
250
        $time = new DateTime();
251
252
        $time->add(new DateInterval('PT' . config()->get('otp_expires') . 'M'));
253
254
        $this->authService->update(
255
            $this->keyFields[AuthKeys::USERNAME],
256
            $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]),
257
            [
258
                $this->keyFields[AuthKeys::OTP] => $otp,
259
                $this->keyFields[AuthKeys::OTP_EXPIRY] => $time->format('Y-m-d H:i'),
260
                $this->keyFields[AuthKeys::OTP_TOKEN] => $otpToken,
261
            ]
262
        );
263
264
        $body = [
265
            'user' => $user,
266
            'code' => $otp
267
        ];
268
269
        $this->mailer->setSubject(t('common.otp'));
270
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'verification');
271
272
        $this->sendMail($user, $body);
273
274
        return $otpToken;
275
    }
276
277
    /**
278
     * Verify and update OTP
279
     * @param int $otp
280
     * @param string $otpToken
281
     * @return User
282
     * @throws AuthException
283
     * @throws Exception
284
     */
285
    protected function verifyAndUpdateOtp(int $otp, string $otpToken): User
286
    {
287
        $user = $this->authService->get($this->keyFields[AuthKeys::OTP_TOKEN], $otpToken);
288
289
        if (!$user || $otp != $user->getFieldValue($this->keyFields[AuthKeys::OTP])) {
290
            throw AuthException::incorrectVerificationCode();
291
        }
292
293
        if (new DateTime() >= new DateTime($user->getFieldValue($this->keyFields[AuthKeys::OTP_EXPIRY]))) {
294
            throw AuthException::verificationCodeExpired();
295
        }
296
297
        $this->authService->update(
298
            $this->keyFields[AuthKeys::USERNAME],
299
            $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]),
300
            [
301
                $this->keyFields[AuthKeys::OTP] => null,
302
                $this->keyFields[AuthKeys::OTP_EXPIRY] => null,
303
                $this->keyFields[AuthKeys::OTP_TOKEN] => null,
304
            ]
305
        );
306
307
        return $user;
308
    }
309
310
    /**
311
     * Filters and gets the visible fields
312
     * @param User $user
313
     * @return array
314
     */
315
    protected function getVisibleFields(User $user): array
316
    {
317
        $userData = $user->getData();
318
319
        if (count($this->visibleFields)) {
320
            foreach ($userData as $field => $value) {
321
                if (!in_array($field, $this->visibleFields)) {
322
                    unset($userData[$field]);
323
                }
324
            }
325
        }
326
327
        return $userData;
328
    }
329
330
    /**
331
     * Is user account activated
332
     * @param User $user
333
     * @return bool
334
     */
335
    protected function isActivated(User $user): bool
336
    {
337
        return empty($user->getFieldValue($this->keyFields[AuthKeys::ACTIVATION_TOKEN]));
338
    }
339
340
    /**
341
     * Generate Token
342
     * @param string|null $val
343
     * @return string
344
     */
345
    protected function generateToken(string $val = null): string
346
    {
347
        return base64_encode($this->hasher->hash($val ?: config()->get('app_key')));
0 ignored issues
show
Bug introduced by
It seems like $val ?: config()->get('app_key') can also be of type null; however, parameter $password of Quantum\Libraries\Hasher\Hasher::hash() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

347
        return base64_encode($this->hasher->hash(/** @scrutinizer ignore-type */ $val ?: config()->get('app_key')));
Loading history...
348
    }
349
350
    /**
351
     * Send email
352
     * @param User $user
353
     * @param array $body
354
     */
355
    protected function sendMail(User $user, array $body)
356
    {
357
        $fullName = ($user->hasField('firstname') && $user->hasField('lastname')) ? $user->getFieldValue('firstname') . ' ' . $user->getFieldValue('lastname') : '';
358
359
        $appEmail = config()->get('app_email') ?: '';
360
        $appName = config()->get('app_name') ?: '';
361
362
        $this->mailer->setFrom($appEmail, $appName)
363
            ->setAddress($user->getFieldValue($this->keyFields[AuthKeys::USERNAME]), $fullName)
0 ignored issues
show
Bug introduced by
It seems like $user->getFieldValue($th...ts\AuthKeys::USERNAME]) can also be of type null; however, parameter $email of Quantum\Libraries\Mailer...Interface::setAddress() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

363
            ->setAddress(/** @scrutinizer ignore-type */ $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]), $fullName)
Loading history...
364
            ->setBody($body)
365
            ->send();
366
    }
367
368
    /**
369
     * Verify user schema
370
     * @param array $schema
371
     * @throws AuthException
372
     */
373
    protected function verifySchema(array $schema)
374
    {
375
        $constants = (new ReflectionClass(AuthKeys::class))->getConstants();
376
377
        foreach ($constants as $constant) {
378
            if (!isset($schema[$constant]) || !isset($schema[$constant]['name'])) {
379
                throw AuthException::incorrectUserSchema();
380
            }
381
382
            $this->keyFields[$constant] = $schema[$constant]['name'];
383
        }
384
385
        foreach ($schema as $field) {
386
            if (isset($field['visible']) && $field['visible']) {
387
                $this->visibleFields[] = $field['name'];
388
            }
389
        }
390
    }
391
392
    /**
393
     * @return bool
394
     */
395
    protected function isTwoFactorEnabled(): bool
396
    {
397
        return filter_var(config()->get('TWO_FA'), FILTER_VALIDATE_BOOLEAN);
398
    }
399
}