Failed Conditions
Pull Request — master (#229)
by Rafael
02:34
created

AuthController::newAuthSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 21
ccs 0
cts 12
cp 0
crap 2
rs 9.584
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Canvas\Api\Controllers;
6
7
use Canvas\Models\Users;
8
use Canvas\Models\Sources;
9
use Canvas\Models\UserLinkedSources;
10
use Canvas\Exception\ServerErrorHttpException;
11
use Canvas\Exception\ModelException;
12
use Baka\Auth\Models\Users as BakaUsers;
13
use Canvas\Traits\AuthTrait;
14
use Canvas\Traits\SocialLoginTrait;
15
use Exception;
16
use Phalcon\Http\Response;
17
use Phalcon\Validation\Validator\Confirmation;
18
use Phalcon\Validation\Validator\Email as EmailValidator;
19
use Phalcon\Validation\Validator\PresenceOf;
20
use Phalcon\Validation\Validator\StringLength;
21
use Baka\Auth\Models\Sessions;
22
use Canvas\Auth\Factory;
23
use Canvas\Validation as CanvasValidation;
24
use Canvas\Notifications\ResetPassword;
25
use Canvas\Notifications\PasswordUpdate;
26
use Canvas\Validations\PasswordValidation;
27
use Canvas\Traits\TokenTrait;
28
use Lcobucci\JWT\Builder;
29
use Lcobucci\JWT\Signer\Hmac\Sha512;
30
31
/**
32
 * Class AuthController.
33
 *
34
 * @package Canvas\Api\Controllers
35
 *
36
 * @property Users $userData
37
 * @property Request $request
38
 * @property Config $config
39
 * @property \Baka\Mail\Message $mail
40
 * @property Apps $app
41
 */
42
class AuthController extends \Baka\Auth\AuthController
43
{
44
    /**
45
     * Auth Trait.
46
     */
47
    use AuthTrait;
48
    use TokenTrait;
49
    use SocialLoginTrait;
50
51
    /**
52
     * Setup for this controller.
53
     *
54
     * @return void
55
     */
56
    public function onConstruct()
57
    {
58
        $this->userLinkedSourcesModel = new UserLinkedSources();
59
        $this->userModel = new Users();
60
61
        if (!isset($this->config->jwt)) {
62
            throw new ServerErrorHttpException('You need to configure your app JWT');
0 ignored issues
show
Deprecated Code introduced by
The class Canvas\Exception\ServerErrorHttpException has been deprecated with message: version 0.1.5

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
63
        }
64
    }
65
66
    /**
67
     * User Login.
68
     * @method POST
69
     * @url /v1/auth
70
     *
71
     * @return Response
72
     */
73
    public function login() : Response
74
    {
75
        $request = $this->request->getPostData();
76
77
        $userIp = !defined('API_TESTS') ? $this->request->getClientAddress() : '127.0.0.1'; //help getting the client ip on scrutinizer :(
78
        $admin = 0;
79
        $remember = 1;
80
81
        //Ok let validate user password
82
        $validation = new CanvasValidation();
83
        $validation->add('email', new EmailValidator(['message' => _('The email is not valid')]));
84
        $validation->add('password', new PresenceOf(['message' => _('The password is required.')]));
85
86
        $validation->setFilters('name', 'trim');
87
        $validation->setFilters('password', 'trim');
88
89
        //validate this form for password
90
        $validation->validate($request);
91
92
        $email = $validation->getValue('email');
93
        $password = $validation->getValue('password');
94
95
        /**
96
         * Login the user via ecosystem or app.
97
         */
98
        $auth = Factory::create($this->app->ecosystemAuth());
99
        $userData = $auth::login($email, $password, $remember, $admin, $userIp);
100
        $token = $userData->getToken();
101
102
        //start session
103
        $session = new Sessions();
104
        $session->start($userData, $token['sessionId'], $token['token'], $userIp, 1);
105
106
        return $this->response([
107
            'token' => $token['token'],
108
            'refresh_token' => $token['refresh_token'],
109
            'time' => date('Y-m-d H:i:s'),
110
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
111
            'refresh_token_expires' => date('Y-m-d H:i:s', time() + 31536000),
112
            'id' => $userData->getId()
113
        ]);
114
    }
115
116
    /**
117
     * User Signup.
118
     *
119
     * @method POST
120
     * @url /v1/users
121
     *
122
     * @return Response
123
     */
124
    public function signup() : Response
125
    {
126
        $user = $this->userModel;
127
128
        $request = $this->request->getPostData();
129
130
        //Ok let validate user password
131
        $validation = new CanvasValidation();
132
        $validation->add('password', new PresenceOf(['message' => _('The password is required.')]));
133
        $validation->add('firstname', new PresenceOf(['message' => _('The firstname is required.')]));
134
        $validation->add('lastname', new PresenceOf(['message' => _('The lastname is required.')]));
135
        $validation->add('email', new EmailValidator(['message' => _('The email is not valid.')]));
136
137
        $validation->add(
138
            'password',
139
            new StringLength([
140
                'min' => 8,
141
                'messageMinimum' => _('Password is too short. Minimum 8 characters.'),
142
            ])
143
        );
144
145
        $validation->add('password', new Confirmation([
146
            'message' => _('Password and confirmation do not match.'),
147
            'with' => 'verify_password',
148
        ]));
149
150
        $validation->setFilters('password', 'trim');
151
        $validation->setFilters('displayname', 'trim');
152
        $validation->setFilters('default_company', 'trim');
153
154
        //validate this form for password
155
        $validation->validate($request);
156
157
        $user->email = $validation->getValue('email');
158
        $user->firstname = $validation->getValue('firstname');
159
        $user->lastname = $validation->getValue('lastname');
160
        $user->password = $validation->getValue('password');
161
        $userIp = !defined('API_TESTS') ? $this->request->getClientAddress() : '127.0.0.1'; //help getting the client ip on scrutinizer :(
162
        $user->displayname = $validation->getValue('displayname');
163
        $user->defaultCompanyName = $validation->getValue('default_company');
164
165
        //user registration
166
        try {
167
            $this->db->begin();
168
169
            $user->signUp();
170
171
            $this->db->commit();
172
        } catch (Exception $e) {
173
            $this->db->rollback();
174
175
            throw new Exception($e->getMessage());
176
        }
177
178
        $token = $user->getToken();
179
180
        //start session
181
        $session = new Sessions();
182
        $session->start($user, $token['sessionId'], $token['token'], $userIp, 1);
183
184
        $authSession = [
185
            'token' => $token['token'],
186
            'time' => date('Y-m-d H:i:s'),
187
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
188
            'id' => $user->getId(),
189
        ];
190
191
        $user->password = null;
192
        $this->sendEmail($user, 'signup');
193
194
        return $this->response([
195
            'user' => $user,
196
            'session' => $authSession
197
        ]);
198
    }
199
200
    /**
201
     * Refresh user auth.
202
     *
203
     * @return Response
204
     * @todo Validate acces_token and refresh token, session's user email and relogin
205
     */
206
    public function refresh(): Response
207
    {
208
        $request = $this->request->getPostData();
209
        $accessToken = $this->getToken($request['access_token']);
210
        $refreshToken = $this->getToken($request['refresh_token']);
211
212
        // if (time() != $accessToken->getClaim('exp')) {
213
        //     throw new ServerErrorHttpException('Issued Access Token has not expired');
214
        // }
215
216
        //Check if both tokens relate to the same user's email
217
        if ($accessToken->getClaim('sessionId') == $refreshToken->getClaim('sessionId')) {
218
            $user = Users::getByEmail($accessToken->getClaim('email'));
219
        }
220
221
        $token = $this->checkSessionStatus($user, $refreshToken->getClaim('sessionId'), (string)$this->request->getClientAddress());
0 ignored issues
show
Bug introduced by
The variable $user does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
222
223
        return $this->response([
224
            'token' => $token['token'],
225
            'time' => date('Y-m-d H:i:s'),
226
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
227
            'id' => $user->getId(),
228
        ]);
229
    }
230
231
    /**
232
     * Check auth session status and create a new one if there is none.
233
     *
234
     * @param Users $user
235
     * @param string $sessionId
236
     * @param string $clientAddress
237
     * @return array
238
     */
239
    private function checkSessionStatus(Users $user, string $sessionId, string $clientAddress): array
240
    {
241
        $session = new Sessions();
242
        $session->check($user, $sessionId, $clientAddress, 1);
243
        $token = $this->newAuthSession($sessionId, $user->email);
244
        $session->start($user, $token['sessionId'], $token['token'], $clientAddress, 1);
245
        return $token;
246
    }
247
248
    /**
249
     * Create a new session based off the refresh token session id.
250
     *
251
     * @param string $sessionId
252
     * @param string $email
253
     * @return void
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
254
     */
255
    private function newAuthSession(string $sessionId, string $email)
256
    {
257
        $signer = new Sha512();
258
        $builder = new Builder();
259
        $token = $builder
0 ignored issues
show
Deprecated Code introduced by
The method Lcobucci\JWT\Builder::setIssuer() has been deprecated with message: This method will be removed on v4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
Deprecated Code introduced by
The method Lcobucci\JWT\Builder::setAudience() has been deprecated with message: This method will be removed on v4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
260
            ->setIssuer(getenv('TOKEN_AUDIENCE'))
261
            ->setAudience(getenv('TOKEN_AUDIENCE'))
262
            ->setId($sessionId, true)
263
            ->setIssuedAt(time())
264
            ->setNotBefore(time() + 500)
265
            ->setExpiration(time() + $this->di->getConfig()->jwt->payload->exp)
266
            ->set('sessionId', $sessionId)
267
            ->set('email', $email)
268
            ->sign($signer, getenv('TOKEN_PASSWORD'))
269
            ->getToken();
270
271
        return [
272
            'sessionId' => $sessionId,
273
            'token' => $token->__toString()
274
        ];
275
    }
276
277
    /**
278
     * Send email to change current email for user.
279
     * @param int $id
280
     * @return Response
281
     */
282
    public function sendEmailChange(int $id): Response
283
    {
284
        //Search for user
285
        $user = Users::getById($id);
286
287
        if (!is_object($user)) {
288
            throw new NotFoundHttpException(_('User not found'));
289
        }
290
291
        //Send email
292
        $this->sendEmail($user, 'email-change');
293
294
        return $this->response($user);
295
    }
296
297
    /**
298
     * Change user's email.
299
     * @param string $hash
300
     * @return Response
301
     */
302
    public function changeUserEmail(string $hash): Response
303
    {
304
        $request = $this->request->getPostData();
305
306
        //Ok let validate user password
307
        $validation = new CanvasValidation();
308
        $validation->add('password', new PresenceOf(['message' => _('The password is required.')]));
309
        $validation->add('new_email', new EmailValidator(['message' => _('The email is not valid.')]));
310
311
        $validation->add(
312
            'password',
313
            new StringLength([
314
                'min' => 8,
315
                'messageMinimum' => _('Password is too short. Minimum 8 characters.'),
316
            ])
317
        );
318
319
        //validate this form for password
320
        $validation->setFilters('password', 'trim');
321
        $validation->setFilters('default_company', 'trim');
322
        $validation->validate($request);
323
324
        $newEmail = $validation->getValue('new_email');
325
        $password = $validation->getValue('password');
326
327
        //Search user by key
328
        $user = Users::getByUserActivationEmail($hash);
329
330
        if (!is_object($user)) {
331
            throw new NotFoundHttpException(_('User not found'));
332
        }
333
334
        $this->db->begin();
335
336
        $user->email = $newEmail;
337
338
        if (!$user->update()) {
339
            throw new ModelException((string)current($user->getMessages()));
0 ignored issues
show
Deprecated Code introduced by
The class Canvas\Exception\ModelException has been deprecated with message: version 0.1.5

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
340
        }
341
342
        if (!$userData = $this->loginUsers($user->email, $password)) {
343
            $this->db->rollback();
344
        }
345
346
        $this->db->commit();
347
348
        return $this->response($userData);
349
    }
350
351
    /**
352
     * Login user using Access Token.
353
     * @return Response
354
     */
355
    public function loginBySocial(): Response
356
    {
357
        $request = $this->request->getPostData();
358
359
        $source = Sources::findFirstOrFail([
360
            'title = ?0 and is_deleted = 0',
361
            'bind' => [$request['provider']]
362
        ]);
363
364
        return $this->response($this->providerLogin($source, $request['social_id'], $request));
365
    }
366
367
    /**
368
     * Send the user how filled out the form to the specify email
369
     * a link to reset his password.
370
     *
371
     * @return Response
372
     */
373
    public function recover(): Response
374
    {
375
        $request = $this->request->getPostData();
376
377
        $validation = new CanvasValidation();
378
        $validation->add('email', new EmailValidator(['message' => _('The email is not valid.')]));
379
380
        $validation->validate($request);
381
382
        $email = $validation->getValue('email');
383
384
        $recoverUser = Users::getByEmail($email);
385
        $recoverUser->generateForgotHash();
386
387
        $recoverUser->notify(new ResetPassword($recoverUser));
388
389
        return $this->response(_('Check your email to recover your password'));
390
    }
391
392
    /**
393
     * Reset the user password.
394
     * @method PUT
395
     * @url /v1/reset
396
     *
397
     * @return Response
398
     */
399
    public function reset(string $key) : Response
400
    {
401
        //is the key empty or does it existe?
402
        if (empty($key) || !$userData = Users::findFirst(['user_activation_forgot = :key:', 'bind' => ['key' => $key]])) {
403
            throw new Exception(_('This Key to reset password doesn\'t exist'));
404
        }
405
406
        $request = $this->request->getPostData();
407
408
        // Get the new password and the verify
409
        $newPassword = trim($request['new_password']);
410
        $verifyPassword = trim($request['verify_password']);
411
412
        //Ok let validate user password
413
        PasswordValidation::validate($newPassword, $verifyPassword);
414
415
        // Has the password and set it
416
        $userData->resetPassword($newPassword);
417
        $userData->user_activation_forgot = '';
418
        $userData->updateOrFail();
419
420
        //log the user out of the site from all devices
421
        $session = new Sessions();
422
        $session->end($userData);
423
424
        $userData->notify(new PasswordUpdate($userData));
425
426
        return $this->response(_('Password Updated'));
427
    }
428
429
    /**
430
    * Set the email config array we are going to be sending.
431
    *
432
    * @todo deprecated move to notifications
433
    * @param String $emailAction
0 ignored issues
show
Bug introduced by
There is no parameter named $emailAction. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
434
    * @param Users  $user
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $user a bit more specific; maybe use BakaUsers.
Loading history...
435
    * @return void
436
    */
437
    protected function sendEmail(BakaUsers $user, string $type): void
438
    {
439
        $send = true;
440
        $subject = null;
441
        $body = null;
442
        switch ($type) {
443
            case 'recover':
444
                $recoveryLink = $this->config->app->frontEndUrl . '/users/reset-password/' . $user->user_activation_forgot;
445
                $subject = _('Password Recovery');
446
                $body = sprintf(_('Click %shere%s to set a new password for your account.'), '<a href="' . $recoveryLink . '" target="_blank">', '</a>');
447
                // send email to recover password
448
                break;
449
            case 'reset':
450
                $activationUrl = $this->config->app->frontEndUrl . '/user/activate/' . $user->user_activation_key;
451
                $subject = _('Password Updated!');
452
                $body = sprintf(_('Your password was update please, use this link to activate your account: %sActivate account%s'), '<a href="' . $activationUrl . '">', '</a>');
453
                // send email that password was update
454
                break;
455
            case 'email-change':
456
                $emailChangeUrl = $this->config->app->frontEndUrl . '/user/' . $user->user_activation_email . '/email';
457
                $subject = _('Email Change Request');
458
                $body = sprintf(_('Click %shere%s to set a new email for your account.'), '<a href="' . $emailChangeUrl . '">', '</a>');
459
                break;
460
            default:
461
                $send = false;
462
                break;
463
        }
464
465
        if ($send) {
466
            $this->mail
467
            ->to($user->email)
468
            ->subject($subject)
469
            ->content($body)
470
            ->sendNow();
471
        }
472
    }
473
}
474