JWTAuthentication::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 2
b 0
f 0
nc 1
nop 8
dl 0
loc 10
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant
7
 * PHP Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file JWTAuthentication.php
34
 *
35
 *  The Authentication using JWT class
36
 *
37
 *  @package    Platine\Framework\Auth\Authentication
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Auth\Authentication;
49
50
use DateTime;
51
use Platine\Config\Config;
52
use Platine\Container\ContainerInterface;
53
use Platine\Framework\Auth\AuthenticationInterface;
54
use Platine\Framework\Auth\Authorization\PermissionCacheInterface;
55
use Platine\Framework\Auth\Entity\Token;
56
use Platine\Framework\Auth\Entity\User;
57
use Platine\Framework\Auth\Enum\UserStatus;
58
use Platine\Framework\Auth\Exception\AccountLockedException;
59
use Platine\Framework\Auth\Exception\AccountNotFoundException;
60
use Platine\Framework\Auth\Exception\InvalidCredentialsException;
61
use Platine\Framework\Auth\Exception\MissingCredentialsException;
62
use Platine\Framework\Auth\IdentityInterface;
63
use Platine\Framework\Auth\Repository\TokenRepository;
64
use Platine\Framework\Auth\Repository\UserRepository;
65
use Platine\Framework\Security\JWT\Exception\JWTException;
66
use Platine\Framework\Security\JWT\JWT;
67
use Platine\Http\ServerRequestInterface;
68
use Platine\Logger\LoggerInterface;
69
use Platine\Security\Hash\HashInterface;
70
use Platine\Stdlib\Helper\Arr;
71
use Platine\Stdlib\Helper\Str;
72
73
/**
74
 * @class JWTAuthentication
75
 * @package Platine\Framework\Auth\Authentication
76
 * @template T
77
 */
78
class JWTAuthentication implements AuthenticationInterface
79
{
80
    /**
81
     * Create new instance
82
     * @param JWT $jwt
83
     * @param LoggerInterface $logger
84
     * @param Config<T> $config
85
     * @param HashInterface $hash
86
     * @param UserRepository $userRepository
87
     * @param TokenRepository $tokenRepository
88
     * @param ContainerInterface $container
89
     * @param PermissionCacheInterface $permissionCache
90
     */
91
    public function __construct(
92
        protected JWT $jwt,
93
        protected LoggerInterface $logger,
94
        protected Config $config,
95
        protected HashInterface $hash,
96
        protected UserRepository $userRepository,
97
        protected TokenRepository $tokenRepository,
98
        protected ContainerInterface $container,
99
        protected PermissionCacheInterface $permissionCache
100
    ) {
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function getUser(): IdentityInterface
107
    {
108
        if ($this->isLogged() === false) {
109
            throw new AccountNotFoundException('User not logged', 401);
110
        }
111
112
        $payload = $this->jwt->getPayload();
113
        $id = (int) ($payload['sub'] ?? -1);
114
115
        $user = $this->userRepository->find($id);
116
        if ($user === null) {
117
            throw new AccountNotFoundException(
118
                'Can not find the logged user information, may be data is corrupted',
119
                401
120
            );
121
        }
122
123
        return $user;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user returns the type Platine\Orm\Entity which is incompatible with the type-hinted return Platine\Framework\Auth\IdentityInterface.
Loading history...
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129
    public function getPermissions(): array
130
    {
131
        if ($this->isLogged() === false) {
132
            return [];
133
        }
134
135
        $payload = $this->jwt->getPayload();
136
        $roles = $payload['roles'] ?? [];
137
        $permissions = [];
138
        foreach ($roles as $id) {
139
            // TODO: add configuration for prefix
140
            $roleKey = sprintf('api_role_%d', $id);
141
            $pers = $this->permissionCache->get($roleKey, []);
142
            if (is_array($pers)) {
143
                $permissions = [
144
                    ...$pers,
145
                    ...$permissions
146
                ];
147
            }
148
        }
149
150
        if (count($permissions) > 0) {
151
            return array_unique($permissions);
152
        }
153
154
        // May be cache feature not available or expired
155
        // get user permissions from database
156
        return $this->getUserPermissions((int) ($payload['sub'] ?? -1));
157
    }
158
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    public function getId(): int|string
164
    {
165
        if ($this->isLogged() === false) {
166
            throw new AccountNotFoundException('User not logged', 401);
167
        }
168
169
        $payload = $this->jwt->getPayload();
170
        $id = (int) ($payload['sub'] ?? -1);
171
172
        return $id;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function isLogged(): bool
179
    {
180
        $request = null;
181
        if ($this->container->has(ServerRequestInterface::class)) {
182
            /** @var ServerRequestInterface $request */
183
            $request = $this->container->get(ServerRequestInterface::class);
184
        }
185
186
        if ($request === null) {
187
            return false;
188
        }
189
190
        $headerName = $this->config->get('api.auth.headers.name', 'Authorization');
191
        $tokenHeader = $request->getHeaderLine($headerName);
192
        if (empty($tokenHeader)) {
193
            $this->logger->error('API authentication failed missing token header');
194
195
            return false;
196
        }
197
        $tokenType = $this->config->get('api.auth.headers.token_type', 'Bearer');
198
        $secret = $this->config->get('api.sign.secret', '');
199
200
        $token = Str::replaceFirst($tokenType . ' ', '', $tokenHeader);
201
202
        $this->jwt->setSecret($secret);
203
        try {
204
            $this->jwt->decode($token);
205
206
            return true;
207
        } catch (JWTException $ex) {
208
            $this->logger->error('API authentication failed: {message}', [
209
                'message' => $ex->getMessage(),
210
            ]);
211
        }
212
213
        return false;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function login(
220
        array $credentials = [],
221
        bool $remeberMe = false,
222
        bool $withPassword = true
223
    ): array {
224
        if (isset($credentials['username']) === false) {
225
            throw new MissingCredentialsException(
226
                'Missing username information',
227
                401
228
            );
229
        }
230
231
        if ($withPassword && isset($credentials['password']) === false) {
232
            throw new MissingCredentialsException(
233
                'Missing password information',
234
                401
235
            );
236
        }
237
238
        $username = $credentials['username'];
239
        $password = $credentials['password'] ?? '';
240
        $user = $this->getUserEntity($username, $password, $withPassword);
241
242
        if ($user === null) {
243
            throw new AccountNotFoundException(
244
                sprintf(
245
                    'Can not find the user [%s]',
246
                    $username
247
                ),
248
                401
249
            );
250
        } elseif ($user->status === UserStatus::LOCKED) {
0 ignored issues
show
Bug Best Practice introduced by
The property status does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
251
            throw new AccountLockedException(
252
                sprintf('User [%s] is locked', $username),
253
                401
254
            );
255
        }
256
257
        if ($withPassword && $this->hash->verify($password, $user->password) === false) {
0 ignored issues
show
Bug Best Practice introduced by
The property password does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
258
            throw new InvalidCredentialsException(
259
                sprintf('Invalid credentials for user [%s]', $username),
260
                401
261
            );
262
        }
263
264
        $permissions = $this->getUserPermissions($user);
265
        $roles = Arr::getColumn($user->roles, 'id');
0 ignored issues
show
Bug Best Practice introduced by
The property roles does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
266
        $secret = $this->config->get('api.sign.secret');
267
        $expire = $this->config->get('api.auth.token_expire', 900);
268
        $refreshExpire = $this->config->get('api.auth.refresh_token_expire', 30 * 86400);
269
        $tokenExpire = time() + $expire;
270
        $refreshTokenExpire = time() + $refreshExpire;
271
        $this->jwt->setSecret($secret)
272
                  ->setPayload([
273
                      'sub' => $user->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
274
                      'exp' => $tokenExpire,
275
                      'roles' => $roles,
276
                  ])
277
                  ->sign();
278
279
        $refreshToken = Str::random(128);
280
        $jwtToken = $this->jwt->getToken();
281
282
        $token = $this->tokenRepository->create([
283
            'refresh_token' => $refreshToken,
284
            'expire_at' => (new DateTime())->setTimestamp($refreshTokenExpire),
285
            'user_id' => $user->id,
286
        ]);
287
        $this->tokenRepository->save($token);
288
289
        $data = [
290
          'user' => [
291
            'id' => $user->id,
292
            'username' => $user->username,
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
293
            'lastname' => $user->lastname,
0 ignored issues
show
Bug Best Practice introduced by
The property lastname does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
294
            'firstname' => $user->firstname,
0 ignored issues
show
Bug Best Practice introduced by
The property firstname does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
295
            'email' => $user->email,
0 ignored issues
show
Bug Best Practice introduced by
The property email does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
296
            'status' => $user->status,
297
          ],
298
          'permissions' => $permissions,
299
          'token' => $jwtToken,
300
          'refresh_token' => $refreshToken,
301
        ];
302
303
        return array_merge($data, $this->getUserData($user, $token));
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function logout(bool $destroy = true): void
310
    {
311
        // do nothing now
312
    }
313
314
    /**
315
     * Return the user entity
316
     * @param string $username
317
     * @param string $password
318
     * @param bool $withPassword wether to use password to login
319
     * @return User|null
320
     */
321
    protected function getUserEntity(
322
        string $username,
323
        string $password,
0 ignored issues
show
Unused Code introduced by
The parameter $password is not used and could be removed. ( Ignorable by Annotation )

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

323
        /** @scrutinizer ignore-unused */ string $password,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
324
        bool $withPassword = true
0 ignored issues
show
Unused Code introduced by
The parameter $withPassword is not used and could be removed. ( Ignorable by Annotation )

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

324
        /** @scrutinizer ignore-unused */ bool $withPassword = true

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
325
    ): ?User {
326
        return $this->userRepository->with('roles.permissions')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->userReposi...sername' => $username)) could return the type Platine\Orm\Entity which includes types incompatible with the type-hinted return Platine\Framework\Auth\Entity\User|null. Consider adding an additional type-check to rule them out.
Loading history...
327
                                    ->findBy(['username' => $username]);
328
    }
329
330
    /**
331
     * Return the user additional data
332
     * @param User $user
333
     * @param Token $token
334
     * @return array<string, mixed>
335
     */
336
    protected function getUserData(User $user, Token $token): array
0 ignored issues
show
Unused Code introduced by
The parameter $user is not used and could be removed. ( Ignorable by Annotation )

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

336
    protected function getUserData(/** @scrutinizer ignore-unused */ User $user, Token $token): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $token is not used and could be removed. ( Ignorable by Annotation )

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

336
    protected function getUserData(User $user, /** @scrutinizer ignore-unused */ Token $token): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
337
    {
338
        return [];
339
    }
340
341
    /**
342
     * Return the permission list of the given user
343
     * @param User|int $user
344
     * @return string[]
345
     */
346
    protected function getUserPermissions(User|int $user): array
347
    {
348
        $permissions = [];
349
        if (is_int($user)) {
350
            $user = $this->userRepository->with('roles.permissions')
351
                                         ->find($user);
352
            if ($user === null) {
353
                return [];
354
            }
355
        }
356
357
        $roles = $user->roles;
0 ignored issues
show
Bug Best Practice introduced by
The property roles does not exist on Platine\Framework\Auth\Entity\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
358
        foreach ($roles as $role) {
359
            $rolePermissions = $role->permissions;
360
            foreach ($rolePermissions as $permission) {
361
                if (in_array($permission->code, $permissions) === false) {
362
                    $permissions[] = $permission->code;
363
                }
364
            }
365
        }
366
367
        return $permissions;
368
    }
369
}
370