Passed
Push — develop ( 87ef83...1cf4ee )
by nguereza
02:55
created

JWTAuthentication::getUserEntity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
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\Framework\Auth\ApiAuthenticationInterface;
53
use Platine\Framework\Auth\Entity\Token;
54
use Platine\Framework\Auth\Entity\User;
55
use Platine\Framework\Auth\Exception\AccountLockedException;
56
use Platine\Framework\Auth\Exception\AccountNotFoundException;
57
use Platine\Framework\Auth\Exception\InvalidCredentialsException;
58
use Platine\Framework\Auth\Exception\MissingCredentialsException;
59
use Platine\Framework\Auth\IdentityInterface;
60
use Platine\Framework\Auth\Repository\TokenRepository;
61
use Platine\Framework\Auth\Repository\UserRepository;
62
use Platine\Framework\Security\JWT\Exception\JWTException;
63
use Platine\Framework\Security\JWT\JWT;
64
use Platine\Http\ServerRequestInterface;
65
use Platine\Logger\LoggerInterface;
66
use Platine\Security\Hash\HashInterface;
67
use Platine\Stdlib\Helper\Str;
68
69
/**
70
 * @class JWTAuthentication
71
 * @package Platine\Framework\Auth\Authentication
72
 * @template T
73
 */
74
class JWTAuthentication implements ApiAuthenticationInterface
75
{
76
    /**
77
     * The JWT instance
78
     * @var JWT
79
     */
80
    protected JWT $jwt;
81
82
    /**
83
     * The logger instance
84
     * @var LoggerInterface
85
     */
86
    protected LoggerInterface $logger;
87
88
    /**
89
     * The configuration instance
90
     * @var Config<T>
91
     */
92
    protected Config $config;
93
94
    /**
95
     * The user repository instance
96
     * @var UserRepository
97
     */
98
    protected UserRepository $userRepository;
99
100
    /**
101
     * The token repository
102
     * @var TokenRepository
103
     */
104
    protected TokenRepository $tokenRepository;
105
106
    /**
107
     * Hash instance to use
108
     * @var HashInterface
109
     */
110
    protected HashInterface $hash;
111
112
    /**
113
     * The server request instance
114
     * @var ServerRequestInterface
115
     */
116
    protected ServerRequestInterface $request;
117
118
    /**
119
     * Create new instance
120
     * @param JWT $jwt
121
     * @param LoggerInterface $logger
122
     * @param Config<T> $config
123
     * @param HashInterface $hash
124
     * @param UserRepository $userRepository
125
     * @param TokenRepository $tokenRepository
126
     * @param ServerRequestInterface $request
127
     */
128
    public function __construct(
129
        JWT $jwt,
130
        LoggerInterface $logger,
131
        Config $config,
132
        HashInterface $hash,
133
        UserRepository $userRepository,
134
        TokenRepository $tokenRepository,
135
        ServerRequestInterface $request
136
    ) {
137
        $this->jwt = $jwt;
138
        $this->logger = $logger;
139
        $this->config = $config;
140
        $this->hash = $hash;
141
        $this->userRepository = $userRepository;
142
        $this->tokenRepository = $tokenRepository;
143
        $this->request = $request;
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function getUser(): IdentityInterface
150
    {
151
        if (!$this->isAuthenticated($this->request)) {
152
            throw new AccountNotFoundException('User not logged', 401);
153
        }
154
155
        $payload = $this->jwt->getPayload();
156
        $id = (int) ($payload['sub'] ?? -1);
157
158
        $user = $this->userRepository->find($id);
159
160
        if (!$user) {
161
            throw new AccountNotFoundException(
162
                'Can not find the logged user information, may be data is corrupted',
163
                401
164
            );
165
        }
166
167
        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...
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function isAuthenticated(ServerRequestInterface $request): bool
174
    {
175
        $headerName = $this->config->get('api.auth.headers.name', 'Authorization');
176
        $tokenHeader = $request->getHeaderLine($headerName);
177
        if (empty($tokenHeader)) {
178
            $this->logger->error('API authentication failed missing token header');
179
180
            return false;
181
        }
182
        $tokenType = $this->config->get('api.auth.headers.token_type', 'Bearer');
183
        $secret = $this->config->get('api.sign.secret', '');
184
185
        $token = Str::replaceFirst($tokenType . ' ', '', $tokenHeader);
186
187
        $this->jwt->setSecret($secret);
188
        try {
189
            $this->jwt->decode($token);
190
191
            return true;
192
        } catch (JWTException $ex) {
193
            $this->logger->error('API authentication failed: {message}', [
194
                'message' => $ex->getMessage(),
195
            ]);
196
        }
197
198
        return false;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function login(array $credentials = []): array
205
    {
206
        if (!isset($credentials['username']) || !isset($credentials['password'])) {
207
            throw new MissingCredentialsException(
208
                'Missing username or password information',
209
                401
210
            );
211
        }
212
213
        $username = $credentials['username'];
214
        $password = $credentials['password'];
215
        $user = $this->getUserEntity($username, $password);
216
217
        if ($user === null) {
218
            throw new AccountNotFoundException('Can not find the user with the given information', 401);
219
        } elseif ($user->status === 'D') {
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...
220
            throw new AccountLockedException(
221
                'User is locked',
222
                401
223
            );
224
        }
225
226
        if (!$this->hash->verify($password, $user->password)) {
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...
227
            throw new InvalidCredentialsException(
228
                'Invalid credentials',
229
                401
230
            );
231
        }
232
233
        $permissions = [];
234
235
        $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...
236
        foreach ($roles as $role) {
237
            $rolePermissions = $role->permissions;
238
            foreach ($rolePermissions as $permission) {
239
                if (!in_array($permission->code, $permissions)) {
240
                    $permissions[] = $permission->code;
241
                }
242
            }
243
        }
244
245
        $secret = $this->config->get('api.sign.secret');
246
        $expire = $this->config->get('api.auth.token_expire', 900);
247
        $refreshExpire = $this->config->get('api.auth.refresh_token_expire', 30 * 86400);
248
        $tokenExpire = time() + $expire;
249
        $refreshTokenExpire = time() + $refreshExpire;
250
        $this->jwt->setSecret($secret)
0 ignored issues
show
Bug introduced by
It seems like $secret can also be of type null; however, parameter $secret of Platine\Framework\Security\JWT\JWT::setSecret() 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

250
        $this->jwt->setSecret(/** @scrutinizer ignore-type */ $secret)
Loading history...
251
                  ->setPayload([
252
                      '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...
253
                      'exp' => $tokenExpire,
254
                      'permissions' => $permissions,
255
                  ])
256
                  ->sign();
257
        $refreshToken = Str::randomToken(24);
258
        $jwtToken = $this->jwt->getToken();
259
260
        $token = $this->tokenRepository->create([
261
            'token' => $jwtToken,
262
            'refresh_token' => $refreshToken,
263
            'expire_at' => (new DateTime())->setTimestamp($refreshTokenExpire),
264
            'user_id' => $user->id,
265
        ]);
266
267
        $this->tokenRepository->save($token);
268
269
        $data = [
270
          'user' => [
271
            'id' => $user->id,
272
            '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...
273
            '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...
274
            '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...
275
            '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...
276
            'permissions' => $permissions,
277
          ],
278
          'token' => $jwtToken,
279
          'refresh_token' => $refreshToken,
280
        ];
281
282
        return array_merge($data, $this->getUserData($user, $token));
283
    }
284
285
    /**
286
     * Return the user entity
287
     * @param string $username
288
     * @param string $password
289
     * @return User|null
290
     */
291
    protected function getUserEntity(string $username, string $password): ?User
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

291
    protected function getUserEntity(string $username, /** @scrutinizer ignore-unused */ string $password): ?User

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...
292
    {
293
        return $this->userRepository
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...
294
                                    ->with('roles.permissions')
295
                                    ->findBy(['username' => $username]);
296
    }
297
298
    /**
299
     * Return the user additional data
300
     * @param User $user
301
     * @param Token $token
302
     * @return array<string, mixed>
303
     */
304
    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

304
    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...
305
    {
306
        return [
307
            'token_expire' => $token->expire_at->getTimestamp()
0 ignored issues
show
Bug Best Practice introduced by
The property expire_at does not exist on Platine\Framework\Auth\Entity\Token. Since you implemented __get, consider adding a @property annotation.
Loading history...
308
        ];
309
    }
310
}
311