Passed
Push — develop ( 36c2b4...1d782e )
by nguereza
03:02
created

JWT::getPayload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant PHP
7
 * 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 JWT.php
34
 *
35
 *  The JWT class
36
 *
37
 *  @package    Platine\Framework\Security\JWT
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   http://www.iacademy.cf
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Security\JWT;
49
50
use DateTime;
51
use Platine\Framework\Security\JWT\Encoder\Base64UrlSafeEncoder;
52
use Platine\Framework\Security\JWT\Exception\InvalidTokenException;
53
use Platine\Framework\Security\JWT\Exception\JWTException;
54
use Platine\Framework\Security\JWT\Exception\TokenExpiredException;
55
56
/**
57
 * @class JWT
58
 * @package Platine\Framework\Security\JWT
59
 */
60
class JWT
61
{
62
63
    /**
64
     * The payload
65
     * @var array<string, mixed>
66
     */
67
    protected array $payload = [];
68
69
    /**
70
     * The JWT headers
71
     * @var array<string, mixed>
72
     */
73
    protected array $headers = [];
74
75
    /**
76
     * The encoder instance
77
     * @var EncoderInterface
78
     */
79
    protected EncoderInterface $encoder;
80
81
    /**
82
     * The signer instance
83
     * @var SignerInterface
84
     */
85
    protected SignerInterface $signer;
86
87
    /**
88
     * The signature
89
     * @var string
90
     */
91
    protected string $signature;
92
93
    /**
94
     * The encoded signature
95
     * @var string
96
     */
97
    protected string $encodedSignature;
98
99
    /**
100
     * Whether already generate signature
101
     * @var bool
102
     */
103
    protected bool $signed = false;
104
105
    /**
106
     * The original JWT token
107
     * @var string
108
     */
109
    protected string $originalToken;
110
111
    /**
112
     * The secret key to used to sign the token
113
     * @var string
114
     */
115
    protected string $secret = '';
116
117
    /**
118
     * Create new instance
119
     * @param SignerInterface $signer
120
     * @param EncoderInterface|null $encoder
121
     */
122
    public function __construct(
123
        SignerInterface $signer,
124
        ?EncoderInterface $encoder = null
125
    ) {
126
        $this->encoder = $encoder ?? new Base64UrlSafeEncoder();
127
        $this->signer = $signer;
128
    }
129
130
    /**
131
     * Decode the JWT instance based on the given token
132
     * @param string $token
133
     * @return $this
134
     */
135
    public function decode(string $token): self
136
    {
137
        $parts = explode('.', $token);
138
139
        if (count($parts) === 3) {
140
            $headers = json_decode($this->encoder->decode($parts[0]), true);
141
            $payload = json_decode($this->encoder->decode($parts[1]), true);
142
            if (is_array($headers) && is_array($payload)) {
143
                $algo = $headers['alg'] ?? '';
144
                if (empty($algo) || $algo !== $this->signer->getTokenAlgoName()) {
145
                    throw new InvalidTokenException(sprintf(
146
                        'The token [%s] cannot be validated, missing or invalid algorithm',
147
                        $token
148
                    ));
149
                } else {
150
                    $this->setHeaders($headers)
151
                          ->setPayload($payload)
152
                           ->setOriginalToken($token)
153
                            ->setEncodedSignature($parts[2]);
154
155
                    if (!$this->verify()) {
156
                        throw new InvalidTokenException(sprintf(
157
                            'The token [%s] cannot be verified because it is invalid',
158
                            $token
159
                        ));
160
                    }
161
162
                    if ($this->isExpired()) {
163
                        throw new TokenExpiredException(sprintf(
164
                            'The token [%s] is already expired',
165
                            $token
166
                        ));
167
                    }
168
169
                    return $this;
170
                }
171
            }
172
        }
173
174
        throw new InvalidTokenException(sprintf(
175
            'The token [%s] using an invalid JWT format',
176
            $token
177
        ));
178
    }
179
180
    /**
181
     * Verifies that the internal input signature corresponds to the encoded
182
     * signature previously stored (@see $this::load).
183
     * @return bool
184
     */
185
    public function verify(): bool
186
    {
187
        if (empty($this->secret) || empty($this->headers['alg'])) {
188
            return false;
189
        }
190
191
        $decodedSignature = $this->encoder->decode($this->getEncodedSignature());
192
        $tokenSignature = $this->getTokenSignature();
193
194
        return $this->signer->verify($this->secret, $decodedSignature, $tokenSignature);
195
    }
196
197
    /**
198
     * Get the original token signature if it exists, otherwise generate the
199
     * signature input for the current instance
200
     * @return string
201
     */
202
    public function getTokenSignature(): string
203
    {
204
        $parts = explode('.', $this->originalToken);
205
206
        if (count($parts) >= 2) {
207
            return sprintf('%s.%s', $parts[0], $parts[1]);
208
        }
209
210
        return $this->generateSignature();
211
    }
212
213
    /**
214
     * Sign the data
215
     * @return string
216
     */
217
    public function sign(): string
218
    {
219
        $this->signature = $this->signer->sign(
220
            $this->generateSignature(),
221
            $this->secret
222
        );
223
        $this->signed = true;
224
225
        return $this->signature;
226
    }
227
228
    /**
229
     * Return the signature. Note you need call ::sign first before
230
     * call this method
231
     * @return string
232
     */
233
    public function getSignature(): string
234
    {
235
        if ($this->signed) {
236
            return $this->signature;
237
        }
238
239
        throw new JWTException('The data is not yet signed, please sign it first');
240
    }
241
242
    /**
243
     * Whether already signed or not
244
     * @return bool
245
     */
246
    public function isSigned(): bool
247
    {
248
        return $this->signed;
249
    }
250
251
    /**
252
     * Return the JWT token
253
     * @return string
254
     */
255
    public function getToken(): string
256
    {
257
        $signature = $this->generateSignature();
258
259
        return sprintf(
260
            '%s.%s',
261
            $signature,
262
            $this->encoder->encode($this->getSignature())
263
        );
264
    }
265
266
    /**
267
     * Generate the signature
268
     * @return string
269
     */
270
    public function generateSignature(): string
271
    {
272
        $this->setDefaults();
273
274
        $encodedPayload = $this->encoder->encode((string) json_encode(
275
            $this->getPayload(),
276
            JSON_UNESCAPED_SLASHES
277
        ));
278
279
        $encodedHeaders = $this->encoder->encode((string) json_encode(
280
            $this->getHeaders(),
281
            JSON_UNESCAPED_SLASHES
282
        ));
283
284
        return sprintf('%s.%s', $encodedHeaders, $encodedPayload);
285
    }
286
287
    /**
288
     * Whether to current token is valid
289
     * @return bool
290
     */
291
    public function isValid(): bool
292
    {
293
        return $this->verify() && !$this->isExpired();
294
    }
295
296
    /**
297
     * Whether the token already expired
298
     * @return bool
299
     */
300
    public function isExpired(): bool
301
    {
302
        if (isset($this->payload['exp'])) {
303
            $exp = $this->payload['exp'];
304
            $now = new DateTime('now');
305
306
            if (is_int($exp)) {
307
                return ($now->getTimestamp() - $exp) > 0;
308
            }
309
310
            if (is_numeric($exp)) {
311
                return ((float) $now->format('U') - $exp) > 0;
312
            }
313
        }
314
315
        return false;
316
    }
317
318
    /**
319
     * Return the encoded signature
320
     * @return string
321
     */
322
    public function getEncodedSignature(): string
323
    {
324
        return $this->encodedSignature;
325
    }
326
327
    /**
328
     * Return the encoded signature
329
     * @param string $encodedSignature
330
     * @return $this
331
     */
332
    public function setEncodedSignature(string $encodedSignature): self
333
    {
334
        $this->encodedSignature = $encodedSignature;
335
        return $this;
336
    }
337
338
339
    /**
340
     * Get the original JWT token
341
     * @return string
342
     */
343
    public function getOriginalToken(): string
344
    {
345
        return $this->originalToken;
346
    }
347
348
    /**
349
     * Set the original JWT token
350
     * @param string $originalToken
351
     * @return $this
352
     */
353
    public function setOriginalToken(string $originalToken): self
354
    {
355
        $this->originalToken = $originalToken;
356
        return $this;
357
    }
358
359
    /**
360
     * Return the payload
361
     * @return array<string, mixed>
362
     */
363
    public function getPayload(): array
364
    {
365
        return $this->payload;
366
    }
367
368
    /**
369
     * Return the JWT headers
370
     * @return array<string, mixed>
371
     */
372
    public function getHeaders(): array
373
    {
374
        return $this->headers;
375
    }
376
377
    /**
378
     * Return the encoder used
379
     * @return EncoderInterface
380
     */
381
    public function getEncoder(): EncoderInterface
382
    {
383
        return $this->encoder;
384
    }
385
386
    /**
387
     * Return the signer
388
     * @return SignerInterface
389
     */
390
    public function getSigner(): SignerInterface
391
    {
392
        return $this->signer;
393
    }
394
395
    /**
396
     * Set the payload
397
     * @param array<string, mixed> $payload
398
     * @return $this
399
     */
400
    public function setPayload(array $payload): self
401
    {
402
        $this->payload = $payload;
403
        return $this;
404
    }
405
406
    /**
407
     * Set the headers
408
     * @param array<string, mixed> $headers
409
     * @return $this
410
     */
411
    public function setHeaders(array $headers): self
412
    {
413
        $this->headers = $headers;
414
        return $this;
415
    }
416
417
    /**
418
     * Set the encoder
419
     * @param EncoderInterface $encoder
420
     * @return $this
421
     */
422
    public function setEncoder(EncoderInterface $encoder): self
423
    {
424
        $this->encoder = $encoder;
425
        return $this;
426
    }
427
428
    /**
429
     * Set the signer
430
     * @param SignerInterface $signer
431
     * @return $this
432
     */
433
    public function setSigner(SignerInterface $signer): self
434
    {
435
        $this->signer = $signer;
436
        return $this;
437
    }
438
439
    /**
440
     * Return the secret key
441
     * @return string
442
     */
443
    public function getSecret(): string
444
    {
445
        return $this->secret;
446
    }
447
448
    /**
449
     * Set the secret key
450
     * @param string $secret
451
     * @return $this
452
     */
453
    public function setSecret(string $secret): self
454
    {
455
        $this->secret = $secret;
456
        return $this;
457
    }
458
459
460
    /**
461
     * Set default values for headers and payload
462
     * @return void
463
     */
464
    protected function setDefaults(): void
465
    {
466
        if (!isset($this->headers['typ'])) {
467
            $this->headers['typ'] = 'JWT';
468
        }
469
470
        if (!isset($this->headers['alg'])) {
471
            $this->headers['alg'] = $this->signer->getTokenAlgoName();
472
        }
473
474
        if (!isset($this->payload['iat'])) {
475
            $this->payload['iat'] = time();
476
        }
477
    }
478
}
479