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

AuthController::newAuthSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22

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 22
ccs 0
cts 11
cp 0
crap 2
rs 9.568
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');
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
        $this->response($token);
107
108
        return $this->response([
109
            'token' => $token['token'],
110
            'refresh_token' => $token['refresh_token'],
111
            'time' => date('Y-m-d H:i:s'),
112
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
113
            'refresh_token_expires' => date('Y-m-d H:i:s', time() + 31536000),
114
            'id' => $userData->getId(),
115
        ]);
116
    }
117
118
    /**
119
     * User Signup.
120
     *
121
     * @method POST
122
     * @url /v1/users
123
     *
124
     * @return Response
125
     */
126
    public function signup() : Response
127
    {
128
        $user = $this->userModel;
129
130
        $request = $this->request->getPostData();
131
132
        //Ok let validate user password
133
        $validation = new CanvasValidation();
134
        $validation->add('password', new PresenceOf(['message' => _('The password is required.')]));
135
        $validation->add('firstname', new PresenceOf(['message' => _('The firstname is required.')]));
136
        $validation->add('lastname', new PresenceOf(['message' => _('The lastname is required.')]));
137
        $validation->add('email', new EmailValidator(['message' => _('The email is not valid.')]));
138
139
        $validation->add(
140
            'password',
141
            new StringLength([
142
                'min' => 8,
143
                'messageMinimum' => _('Password is too short. Minimum 8 characters.'),
144
            ])
145
        );
146
147
        $validation->add('password', new Confirmation([
148
            'message' => _('Password and confirmation do not match.'),
149
            'with' => 'verify_password',
150
        ]));
151
152
        $validation->setFilters('password', 'trim');
153
        $validation->setFilters('displayname', 'trim');
154
        $validation->setFilters('default_company', 'trim');
155
156
        //validate this form for password
157
        $validation->validate($request);
158
159
        $user->email = $validation->getValue('email');
160
        $user->firstname = $validation->getValue('firstname');
161
        $user->lastname = $validation->getValue('lastname');
162
        $user->password = $validation->getValue('password');
163
        $userIp = !defined('API_TESTS') ? $this->request->getClientAddress() : '127.0.0.1'; //help getting the client ip on scrutinizer :(
164
        $user->displayname = $validation->getValue('displayname');
165
        $user->defaultCompanyName = $validation->getValue('default_company');
166
167
        //user registration
168
        try {
169
            $this->db->begin();
170
171
            $user->signUp();
172
173
            $this->db->commit();
174
        } catch (Exception $e) {
175
            $this->db->rollback();
176
177
            throw new Exception($e->getMessage());
178
        }
179
180
        $token = $user->getToken();
181
182
        //start session
183
        $session = new Sessions();
184
        $session->start($user, $token['sessionId'], $token['token'], $userIp, 1);
185
186
        $authSession = [
187
            'token' => $token['token'],
188
            'time' => date('Y-m-d H:i:s'),
189
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
190
            'id' => $user->getId(),
191
        ];
192
193
        $user->password = null;
194
        $this->sendEmail($user, 'signup');
195
196
        return $this->response([
197
            'user' => $user,
198
            'session' => $authSession
199
        ]);
200
    }
201
202
    /**
203
     * Refresh user auth
204
     *
205
     * @return void
0 ignored issues
show
Documentation introduced by
Should the return type not be Response?

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...
206
     * @todo Validate acces_token and refresh token, session's user email and relogin
207
     */
208
    public function refresh(): Response
209
    {
210
        $request = $this->request->getPostData();
211
        $accessToken =  $this->getToken($request['access_token']);
212
        $refreshToken =  $this->getToken($request['refresh_token']);
213
        
214
        // if (time() != $accessToken->getClaim('exp')) {
215
        //     throw new ServerErrorHttpException('Issued Access Token has not expired');
216
        // }
217
218
        //Check if both tokens relate to the same user's email
219
        if ($accessToken->getClaim('sessionId') == $refreshToken->getClaim('sessionId')) {
220
            $user = Users::findFirstOrFail([
221
                'conditions'=>'email = ?0 and is_deleted = 0',
222
                'bind' => [$accessToken->getClaim('email')]
223
            ]);
224
        }
225
226
        //Check if session is active
227
        $session = new Sessions();
228
        $session->check($user, $refreshToken->getClaim('sessionId'), (string)$this->request->getClientAddress(), 1);
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...
229
        $token = $this->newAuthSession($refreshToken->getClaim('sessionId'), $user->email);
230
        $session->start($user, $token['sessionId'], $token['token'], (string)$this->request->getClientAddress(), 1);
231
232
        return $this->response([
233
            'token' => $token['token'],
234
            'time' => date('Y-m-d H:i:s'),
235
            'expires' => date('Y-m-d H:i:s', time() + $this->config->jwt->payload->exp),
236
            'id' => $user->getId(),
237
        ]);
238
    }
239
240
    /**
241
     * Create a new session based off the refresh token session id
242
     *
243
     * @param string $sessionId
244
     * @param string $email
245
     * @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...
246
     */
247
    private function newAuthSession(string $sessionId, string $email)
248
    {
249
        $signer = new Sha512();
250
        $builder = new Builder();
251
        $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...
252
            ->setIssuer(getenv('TOKEN_AUDIENCE'))
253
            ->setAudience(getenv('TOKEN_AUDIENCE'))
254
            ->setId($sessionId, true)
255
            ->setIssuedAt(time())
256
            ->setNotBefore(time() + 500)
257
            ->setExpiration(time() + $this->di->getConfig()->jwt->payload->exp)
258
            ->set('sessionId', $sessionId)
259
            ->set('email', $email)
260
            ->sign($signer, getenv('TOKEN_PASSWORD'))
261
            ->getToken();
262
263
        return [
264
            'sessionId' => $sessionId,
265
            'token' => $token->__toString()
266
        ];
267
268
    }
269
270
    /**
271
     * Send email to change current email for user.
272
     * @param int $id
273
     * @return Response
274
     */
275
    public function sendEmailChange(int $id): Response
276
    {
277
        //Search for user
278
        $user = Users::getById($id);
279
280
        if (!is_object($user)) {
281
            throw new NotFoundHttpException(_('User not found'));
282
        }
283
284
        //Send email
285
        $this->sendEmail($user, 'email-change');
286
287
        return $this->response($user);
288
    }
289
290
    /**
291
     * Change user's email.
292
     * @param string $hash
293
     * @return Response
294
     */
295
    public function changeUserEmail(string $hash): Response
296
    {
297
        $request = $this->request->getPostData();
298
299
        //Ok let validate user password
300
        $validation = new CanvasValidation();
301
        $validation->add('password', new PresenceOf(['message' => _('The password is required.')]));
302
        $validation->add('new_email', new EmailValidator(['message' => _('The email is not valid.')]));
303
304
        $validation->add(
305
            'password',
306
            new StringLength([
307
                'min' => 8,
308
                'messageMinimum' => _('Password is too short. Minimum 8 characters.'),
309
            ])
310
        );
311
312
        //validate this form for password
313
        $validation->setFilters('password', 'trim');
314
        $validation->setFilters('default_company', 'trim');
315
        $validation->validate($request);
316
317
        $newEmail = $validation->getValue('new_email');
318
        $password = $validation->getValue('password');
319
320
        //Search user by key
321
        $user = Users::getByUserActivationEmail($hash);
322
323
        if (!is_object($user)) {
324
            throw new NotFoundHttpException(_('User not found'));
325
        }
326
327
        $this->db->begin();
328
329
        $user->email = $newEmail;
330
331
        if (!$user->update()) {
332
            throw new ModelException((string)current($user->getMessages()));
333
        }
334
335
        if (!$userData = $this->loginUsers($user->email, $password)) {
336
            $this->db->rollback();
337
        }
338
339
        $this->db->commit();
340
341
        return $this->response($userData);
342
    }
343
344
    /**
345
     * Login user using Access Token.
346
     * @return Response
347
     */
348
    public function loginBySocial(): Response
349
    {
350
        $request = $this->request->getPostData();
351
352
        $source = Sources::findFirstOrFail([
353
            'title = ?0 and is_deleted = 0',
354
            'bind' => [$request['provider']]
355
        ]);
356
357
        return $this->response($this->providerLogin($source, $request['social_id'], $request));
358
    }
359
360
    /**
361
     * Send the user how filled out the form to the specify email
362
     * a link to reset his password.
363
     *
364
     * @return Response
365
     */
366
    public function recover(): Response
367
    {
368
        $request = $this->request->getPostData();
369
370
        $validation = new CanvasValidation();
371
        $validation->add('email', new EmailValidator(['message' => _('The email is not valid.')]));
372
373
        $validation->validate($request);
374
375
        $email = $validation->getValue('email');
376
377
        $recoverUser = Users::getByEmail($email);
378
        $recoverUser->generateForgotHash();
379
380
        $recoverUser->notify(new ResetPassword($recoverUser));
381
382
        return $this->response(_('Check your email to recover your password'));
383
    }
384
385
    /**
386
     * Reset the user password.
387
     * @method PUT
388
     * @url /v1/reset
389
     *
390
     * @return Response
391
     */
392
    public function reset(string $key) : Response
393
    {
394
        //is the key empty or does it existe?
395
        if (empty($key) || !$userData = Users::findFirst(['user_activation_forgot = :key:', 'bind' => ['key' => $key]])) {
396
            throw new Exception(_('This Key to reset password doesn\'t exist'));
397
        }
398
399
        $request = $this->request->getPostData();
400
401
        // Get the new password and the verify
402
        $newPassword = trim($request['new_password']);
403
        $verifyPassword = trim($request['verify_password']);
404
405
        //Ok let validate user password
406
        PasswordValidation::validate($newPassword, $verifyPassword);
407
408
        // Has the password and set it
409
        $userData->resetPassword($newPassword);
410
        $userData->user_activation_forgot = '';
411
        $userData->updateOrFail();
412
413
        //log the user out of the site from all devices
414
        $session = new Sessions();
415
        $session->end($userData);
416
417
        $userData->notify(new PasswordUpdate($userData));
418
419
        return $this->response(_('Password Updated'));
420
    }
421
422
    /**
423
    * Set the email config array we are going to be sending.
424
    *
425
    * @todo deprecated move to notifications
426
    * @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...
427
    * @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...
428
    * @return void
429
    */
430
    protected function sendEmail(BakaUsers $user, string $type): void
431
    {
432
        $send = true;
433
        $subject = null;
434
        $body = null;
435
        switch ($type) {
436
            case 'recover':
437
                $recoveryLink = $this->config->app->frontEndUrl . '/users/reset-password/' . $user->user_activation_forgot;
438
                $subject = _('Password Recovery');
439
                $body = sprintf(_('Click %shere%s to set a new password for your account.'), '<a href="' . $recoveryLink . '" target="_blank">', '</a>');
440
                // send email to recover password
441
                break;
442
            case 'reset':
443
                $activationUrl = $this->config->app->frontEndUrl . '/user/activate/' . $user->user_activation_key;
444
                $subject = _('Password Updated!');
445
                $body = sprintf(_('Your password was update please, use this link to activate your account: %sActivate account%s'), '<a href="' . $activationUrl . '">', '</a>');
446
                // send email that password was update
447
                break;
448
            case 'email-change':
449
                $emailChangeUrl = $this->config->app->frontEndUrl . '/user/' . $user->user_activation_email . '/email';
450
                $subject = _('Email Change Request');
451
                $body = sprintf(_('Click %shere%s to set a new email for your account.'), '<a href="' . $emailChangeUrl . '">', '</a>');
452
                break;
453
            default:
454
                $send = false;
455
                break;
456
        }
457
458
        if ($send) {
459
            $this->mail
460
            ->to($user->email)
461
            ->subject($subject)
462
            ->content($body)
463
            ->sendNow();
464
        }
465
    }
466
}
467