JWTAuthentication::getPermissions()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 15
nc 7
nop 0
dl 0
loc 28
rs 9.4555
c 1
b 0
f 0
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