JWT::setTestTimestamp()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the PHP-JWT package.
7
 *
8
 * (c) Jitendra Adhikari <[email protected]>
9
 *     <https://github.com/adhocore>
10
 *
11
 * Licensed under MIT license.
12
 */
13
14
namespace Ahc\Jwt;
15
16
/**
17
 * JSON Web Token (JWT) implementation in PHP5.5+.
18
 *
19
 * @author   Jitendra Adhikari <[email protected]>
20
 * @license  MIT
21
 *
22
 * @link     https://github.com/adhocore/jwt
23
 */
24
class JWT
25
{
26
    use ValidatesJWT;
27
28
    const ERROR_KEY_EMPTY        = 10;
29
    const ERROR_KEY_INVALID      = 12;
30
    const ERROR_ALGO_UNSUPPORTED = 20;
31
    const ERROR_ALGO_MISSING     = 22;
32
    const ERROR_INVALID_MAXAGE   = 30;
33
    const ERROR_INVALID_LEEWAY   = 32;
34
    const ERROR_JSON_FAILED      = 40;
35
    const ERROR_TOKEN_INVALID    = 50;
36
    const ERROR_TOKEN_EXPIRED    = 52;
37
    const ERROR_TOKEN_NOT_NOW    = 54;
38
    const ERROR_SIGNATURE_FAILED = 60;
39
    const ERROR_KID_UNKNOWN      = 70;
40
41
    /** @var array Supported Signing algorithms. */
42
    protected $algos = [
43
        'HS256' => 'sha256',
44
        'HS384' => 'sha384',
45
        'HS512' => 'sha512',
46
        'RS256' => \OPENSSL_ALGO_SHA256,
47
        'RS384' => \OPENSSL_ALGO_SHA384,
48
        'RS512' => \OPENSSL_ALGO_SHA512,
49
    ];
50
51
    /** @var string|resource The signature key. */
52
    protected $key;
53
54
    /** @var array The list of supported keys with id. */
55
    protected $keys = [];
56
57
    /** @var int|null Use setTestTimestamp() to set custom value for time(). Useful for testability. */
58
    protected $timestamp = null;
59
60
    /** @var string The JWT signing algorithm. Defaults to HS256. */
61
    protected $algo = 'HS256';
62
63
    /** @var int The JWT TTL in seconds. Defaults to 1 hour. */
64
    protected $maxAge = 3600;
65
66
    /** @var int Grace period in seconds to allow for clock skew. Defaults to 0 seconds. */
67
    protected $leeway = 0;
68
69
    /** @var string|null The passphrase for RSA signing (optional). */
70
    protected $passphrase;
71
72
    /**
73
     * Constructor.
74
     *
75
     * @param string|resource $key    The signature key. For RS* it should be file path or resource of private key.
76
     * @param string          $algo   The algorithm to sign/verify the token.
77
     * @param int             $maxAge The TTL of token to be used to determine expiry if `iat` claim is present.
78
     *                                This is also used to provide default `exp` claim in case it is missing.
79
     * @param int             $leeway Leeway for clock skew. Shouldnot be more than 2 minutes (120s).
80
     * @param string          $pass   The passphrase (only for RS* algos).
81
     */
82
    public function __construct(
83
        $key,
84
        string $algo = 'HS256',
85
        int $maxAge = 3600,
86
        int $leeway = 0,
87
        string $pass = null
88
    ) {
89
        $this->validateConfig($key, $algo, $maxAge, $leeway);
90
91
        if (\is_array($key)) {
0 ignored issues
show
introduced by
The condition is_array($key) is always false.
Loading history...
92
            $this->registerKeys($key);
93
            $key = \reset($key); // use first one!
94
        }
95
96
        $this->key        = $key;
97
        $this->algo       = $algo;
98
        $this->maxAge     = $maxAge;
99
        $this->leeway     = $leeway;
100
        $this->passphrase = $pass;
101
    }
102
103
    /**
104
     * Register keys for `kid` support.
105
     *
106
     * @param array $keys Use format: ['<kid>' => '<key data>', '<kid2>' => '<key data2>']
107
     *
108
     * @return self
109
     */
110
    public function registerKeys(array $keys): self
111
    {
112
        $this->keys = \array_merge($this->keys, $keys);
113
114
        return $this;
115
    }
116
117
    /**
118
     * Encode payload as JWT token.
119
     *
120
     * @param array $payload
121
     * @param array $header  Extra header (if any) to append.
122
     *
123
     * @return string URL safe JWT token.
124
     */
125
    public function encode(array $payload, array $header = []): string
126
    {
127
        $header = ['typ' => 'JWT', 'alg' => $this->algo] + $header;
128
129
        $this->validateKid($header);
130
131
        if (!isset($payload['iat']) && !isset($payload['exp'])) {
132
            $payload['exp'] = ($this->timestamp ?: \time()) + $this->maxAge;
133
        }
134
135
        $header    = $this->urlSafeEncode($header);
136
        $payload   = $this->urlSafeEncode($payload);
137
        $signature = $this->urlSafeEncode($this->sign($header . '.' . $payload));
138
139
        return $header . '.' . $payload . '.' . $signature;
140
    }
141
142
    /**
143
     * Decode JWT token and return original payload.
144
     *
145
     * @param string $token
146
     *
147
     * @throws JWTException
148
     *
149
     * @return array
150
     */
151
    public function decode(string $token): array
152
    {
153
        if (\substr_count($token, '.') < 2) {
154
            throw new JWTException('Invalid token: Incomplete segments', static::ERROR_TOKEN_INVALID);
155
        }
156
157
        $token = \explode('.', $token, 3);
158
        $this->validateHeader((array) $this->urlSafeDecode($token[0]));
159
160
        // Validate signature.
161
        if (!$this->verify($token[0] . '.' . $token[1], $token[2])) {
162
            throw new JWTException('Invalid token: Signature failed', static::ERROR_SIGNATURE_FAILED);
163
        }
164
165
        $payload = (array) $this->urlSafeDecode($token[1]);
166
167
        $this->validateTimestamps($payload);
168
169
        return $payload;
170
    }
171
172
    /**
173
     * Spoof current timestamp for testing.
174
     *
175
     * @param int|null $timestamp
176
     */
177
    public function setTestTimestamp(int $timestamp = null): self
178
    {
179
        $this->timestamp = $timestamp;
180
181
        return $this;
182
    }
183
184
    /**
185
     * Sign the input with configured key and return the signature.
186
     *
187
     * @param string $input
188
     *
189
     * @return string
190
     */
191
    protected function sign(string $input): string
192
    {
193
        // HMAC SHA.
194
        if (\substr($this->algo, 0, 2) === 'HS') {
195
            return \hash_hmac($this->algos[$this->algo], $input, $this->key, true);
0 ignored issues
show
Bug introduced by
It seems like $this->key can also be of type resource; however, parameter $key of hash_hmac() 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

195
            return \hash_hmac($this->algos[$this->algo], $input, /** @scrutinizer ignore-type */ $this->key, true);
Loading history...
196
        }
197
198
        $this->validateKey();
199
200
        \openssl_sign($input, $signature, $this->key, $this->algos[$this->algo]);
201
202
        return $signature;
203
    }
204
205
    /**
206
     * Verify the signature of given input.
207
     *
208
     * @param string $input
209
     * @param string $signature
210
     *
211
     * @throws JWTException When key is invalid.
212
     *
213
     * @return bool
214
     */
215
    protected function verify(string $input, string $signature): bool
216
    {
217
        $algo = $this->algos[$this->algo];
218
219
        // HMAC SHA.
220
        if (\substr($this->algo, 0, 2) === 'HS') {
221
            return \hash_equals($this->urlSafeEncode(\hash_hmac($algo, $input, $this->key, true)), $signature);
0 ignored issues
show
Bug introduced by
It seems like $this->key can also be of type resource; however, parameter $key of hash_hmac() 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

221
            return \hash_equals($this->urlSafeEncode(\hash_hmac($algo, $input, /** @scrutinizer ignore-type */ $this->key, true)), $signature);
Loading history...
222
        }
223
224
        $this->validateKey();
225
226
        $pubKey = \openssl_pkey_get_details($this->key)['key'];
0 ignored issues
show
Bug introduced by
It seems like $this->key can also be of type string; however, parameter $key of openssl_pkey_get_details() does only seem to accept resource, 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

226
        $pubKey = \openssl_pkey_get_details(/** @scrutinizer ignore-type */ $this->key)['key'];
Loading history...
227
228
        return \openssl_verify($input, $this->urlSafeDecode($signature, false), $pubKey, $algo) === 1;
229
    }
230
231
    /**
232
     * URL safe base64 encode.
233
     *
234
     * First serialized the payload as json if it is an array.
235
     *
236
     * @param array|string $data
237
     *
238
     * @throws JWTException When JSON encode fails.
239
     *
240
     * @return string
241
     */
242
    protected function urlSafeEncode($data): string
243
    {
244
        if (\is_array($data)) {
245
            $data = \json_encode($data, \JSON_UNESCAPED_SLASHES);
246
            $this->validateLastJson();
247
        }
248
249
        return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
250
    }
251
252
    /**
253
     * URL safe base64 decode.
254
     *
255
     * @param array|string $data
256
     * @param bool         $asJson Whether to parse as JSON (defaults to true).
257
     *
258
     * @throws JWTException When JSON encode fails.
259
     *
260
     * @return array|\stdClass|string
261
     */
262
    protected function urlSafeDecode($data, bool $asJson = true)
263
    {
264
        if (!$asJson) {
265
            return \base64_decode(\strtr($data, '-_', '+/'));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type array; however, parameter $str of strtr() 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

265
            return \base64_decode(\strtr(/** @scrutinizer ignore-type */ $data, '-_', '+/'));
Loading history...
266
        }
267
268
        $data = \json_decode(\base64_decode(\strtr($data, '-_', '+/')));
269
        $this->validateLastJson();
270
271
        return $data;
272
    }
273
}
274