JWT::getEncodedSignature()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
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   https://www.platine-php.com
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
use Platine\Stdlib\Helper\Json;
56
57
/**
58
 * @class JWT
59
 * @package Platine\Framework\Security\JWT
60
 */
61
class JWT
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
        if (count($parts) === 3) {
139
            $headers = Json::decode($this->encoder->decode($parts[0]), true);
140
            $payload = Json::decode($this->encoder->decode($parts[1]), true);
141
            if (is_array($headers) && is_array($payload)) {
142
                $algo = $headers['alg'] ?? '';
143
                if (empty($algo) || $algo !== $this->signer->getTokenAlgoName()) {
144
                    throw new InvalidTokenException(sprintf(
145
                        'The token [%s] cannot be validated, missing or invalid algorithm',
146
                        $token
147
                    ));
148
                } else {
149
                    $this->setHeaders($headers)
150
                         ->setPayload($payload)
151
                         ->setOriginalToken($token)
152
                         ->setEncodedSignature($parts[2]);
153
154
                    if ($this->verify() === false) {
155
                        throw new InvalidTokenException(sprintf(
156
                            'The token [%s] cannot be verified because it is invalid',
157
                            $token
158
                        ));
159
                    }
160
161
                    if ($this->isExpired()) {
162
                        throw new TokenExpiredException(sprintf(
163
                            'The token [%s] is already expired',
164
                            $token
165
                        ));
166
                    }
167
168
                    return $this;
169
                }
170
            }
171
        }
172
173
        throw new InvalidTokenException(sprintf(
174
            'The token [%s] using an invalid JWT format',
175
            $token
176
        ));
177
    }
178
179
    /**
180
     * Verifies that the internal input signature corresponds to the encoded
181
     * signature previously stored (@see $this::load).
182
     * @return bool
183
     */
184
    public function verify(): bool
185
    {
186
        if (empty($this->secret) || empty($this->headers['alg'])) {
187
            return false;
188
        }
189
190
        $decodedSignature = $this->encoder->decode($this->getEncodedSignature());
191
        $tokenSignature = $this->getTokenSignature();
192
193
        return $this->signer->verify(
194
            $this->secret,
195
            $decodedSignature,
196
            $tokenSignature
197
        );
198
    }
199
200
    /**
201
     * Get the original token signature if it exists, otherwise generate the
202
     * signature input for the current instance
203
     * @return string
204
     */
205
    public function getTokenSignature(): string
206
    {
207
        $parts = explode('.', $this->originalToken);
208
        if (count($parts) >= 2) {
209
            return sprintf('%s.%s', $parts[0], $parts[1]);
210
        }
211
212
        return $this->generateSignature();
213
    }
214
215
    /**
216
     * Sign the data
217
     * @return string
218
     */
219
    public function sign(): string
220
    {
221
        $this->signature = $this->signer->sign(
222
            $this->generateSignature(),
223
            $this->secret
224
        );
225
        $this->signed = true;
226
227
        return $this->signature;
228
    }
229
230
    /**
231
     * Return the signature. Note you need call ::sign first before
232
     * call this method
233
     * @return string
234
     */
235
    public function getSignature(): string
236
    {
237
        if ($this->signed) {
238
            return $this->signature;
239
        }
240
241
        throw new JWTException('The data is not yet signed, please sign it first');
242
    }
243
244
    /**
245
     * Whether already signed or not
246
     * @return bool
247
     */
248
    public function isSigned(): bool
249
    {
250
        return $this->signed;
251
    }
252
253
    /**
254
     * Return the JWT token
255
     * @return string
256
     */
257
    public function getToken(): string
258
    {
259
        $signature = $this->generateSignature();
260
261
        return sprintf(
262
            '%s.%s',
263
            $signature,
264
            $this->encoder->encode($this->getSignature())
265
        );
266
    }
267
268
    /**
269
     * Generate the signature
270
     * @return string
271
     */
272
    public function generateSignature(): string
273
    {
274
        $this->setDefaults();
275
276
        $encodedPayload = $this->encoder->encode((string) json_encode(
277
            $this->getPayload(),
278
            JSON_UNESCAPED_SLASHES
279
        ));
280
281
        $encodedHeaders = $this->encoder->encode((string) json_encode(
282
            $this->getHeaders(),
283
            JSON_UNESCAPED_SLASHES
284
        ));
285
286
        return sprintf('%s.%s', $encodedHeaders, $encodedPayload);
287
    }
288
289
    /**
290
     * Whether to current token is valid
291
     * @return bool
292
     */
293
    public function isValid(): bool
294
    {
295
        return $this->verify() && $this->isExpired() === false;
296
    }
297
298
    /**
299
     * Whether the token already expired
300
     * @return bool
301
     */
302
    public function isExpired(): bool
303
    {
304
        if (isset($this->payload['exp'])) {
305
            $exp = $this->payload['exp'];
306
            $now = new DateTime('now');
307
308
            if (is_int($exp)) {
309
                return ($now->getTimestamp() - $exp) > 0;
310
            }
311
312
            if (is_numeric($exp)) {
313
                return ((float) $now->format('U') - $exp) > 0;
314
            }
315
        }
316
317
        return false;
318
    }
319
320
    /**
321
     * Return the encoded signature
322
     * @return string
323
     */
324
    public function getEncodedSignature(): string
325
    {
326
        return $this->encodedSignature;
327
    }
328
329
    /**
330
     * Return the encoded signature
331
     * @param string $encodedSignature
332
     * @return $this
333
     */
334
    public function setEncodedSignature(string $encodedSignature): self
335
    {
336
        $this->encodedSignature = $encodedSignature;
337
        return $this;
338
    }
339
340
341
    /**
342
     * Get the original JWT token
343
     * @return string
344
     */
345
    public function getOriginalToken(): string
346
    {
347
        return $this->originalToken;
348
    }
349
350
    /**
351
     * Set the original JWT token
352
     * @param string $originalToken
353
     * @return $this
354
     */
355
    public function setOriginalToken(string $originalToken): self
356
    {
357
        $this->originalToken = $originalToken;
358
        return $this;
359
    }
360
361
    /**
362
     * Return the payload
363
     * @return array<string, mixed>
364
     */
365
    public function getPayload(): array
366
    {
367
        return $this->payload;
368
    }
369
370
    /**
371
     * Return the JWT headers
372
     * @return array<string, mixed>
373
     */
374
    public function getHeaders(): array
375
    {
376
        return $this->headers;
377
    }
378
379
    /**
380
     * Return the encoder used
381
     * @return EncoderInterface
382
     */
383
    public function getEncoder(): EncoderInterface
384
    {
385
        return $this->encoder;
386
    }
387
388
    /**
389
     * Return the signer
390
     * @return SignerInterface
391
     */
392
    public function getSigner(): SignerInterface
393
    {
394
        return $this->signer;
395
    }
396
397
    /**
398
     * Set the payload
399
     * @param array<string, mixed> $payload
400
     * @return $this
401
     */
402
    public function setPayload(array $payload): self
403
    {
404
        $this->payload = $payload;
405
        return $this;
406
    }
407
408
    /**
409
     * Set the headers
410
     * @param array<string, mixed> $headers
411
     * @return $this
412
     */
413
    public function setHeaders(array $headers): self
414
    {
415
        $this->headers = $headers;
416
        return $this;
417
    }
418
419
    /**
420
     * Set the encoder
421
     * @param EncoderInterface $encoder
422
     * @return $this
423
     */
424
    public function setEncoder(EncoderInterface $encoder): self
425
    {
426
        $this->encoder = $encoder;
427
        return $this;
428
    }
429
430
    /**
431
     * Set the signer
432
     * @param SignerInterface $signer
433
     * @return $this
434
     */
435
    public function setSigner(SignerInterface $signer): self
436
    {
437
        $this->signer = $signer;
438
        return $this;
439
    }
440
441
    /**
442
     * Return the secret key
443
     * @return string
444
     */
445
    public function getSecret(): string
446
    {
447
        return $this->secret;
448
    }
449
450
    /**
451
     * Set the secret key
452
     * @param string $secret
453
     * @return $this
454
     */
455
    public function setSecret(string $secret): self
456
    {
457
        $this->secret = $secret;
458
        return $this;
459
    }
460
461
462
    /**
463
     * Set default values for headers and payload
464
     * @return void
465
     */
466
    protected function setDefaults(): void
467
    {
468
        if (!isset($this->headers['typ'])) {
469
            $this->headers['typ'] = 'JWT';
470
        }
471
472
        if (!isset($this->headers['alg'])) {
473
            $this->headers['alg'] = $this->signer->getTokenAlgoName();
474
        }
475
476
        if (!isset($this->payload['iat'])) {
477
            $this->payload['iat'] = time();
478
        }
479
    }
480
}
481