Issues (21)

src/Security/JWT/Builder.php (7 issues)

Labels
Severity
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Phalcon\Security\JWT;
15
16
use Phalcon\Collection;
17
use Phalcon\Helper\Base64;
18
use Phalcon\Helper\Json;
19
use Phalcon\Security\JWT\Exceptions\ValidatorException;
20
use Phalcon\Security\JWT\Signer\SignerInterface;
21
use Phalcon\Security\JWT\Token\Enum;
22
use Phalcon\Security\JWT\Token\Item;
23
use Phalcon\Security\JWT\Token\Signature;
24
use Phalcon\Security\JWT\Token\Token;
25
26
use function is_array;
27
use function is_string;
28
use function preg_match;
29
use function time;
30
31
/**
32
 * Class Builder
33
 *
34
 * @property Collection      $claims
35
 * @property Collection      $jose
36
 * @property string          $passphrase
37
 * @property SignerInterface $signer
38
 *
39
 * @link https://tools.ietf.org/html/rfc7519
40
 */
41
class Builder
42
{
43
    /**
44
     * @var Collection
45
     */
46
    private $claims;
47
48
    /**
49
     * @var Collection
50
     */
51
    private $jose;
52
53
    /**
54
     * @var string
55
     */
56
    private $passphrase;
57
58
    /**
59
     * @var SignerInterface
60
     */
61
    private $signer;
62
63
    /**
64
     * Builder constructor.
65
     *
66
     * @param SignerInterface $signer
67
     */
68 42
    public function __construct(
69
        SignerInterface $signer
70
    ) {
71 42
        $this->init();
72 42
        $this->signer = $signer;
73 42
        $this->jose->set(
74 42
            Enum::ALGO,
75 42
            $this->signer->getAlgHeader()
76
        );
77 42
    }
78
79
    /**
80
     * @return Builder
81
     */
82 42
    public function init(): Builder
83
    {
84 42
        $this->passphrase = "";
85 42
        $this->claims     = new Collection();
86 42
        $this->jose       = new Collection(
87
            [
88 42
                Enum::TYPE => "JWT",
89 42
                Enum::ALGO => "none",
90
            ]
91
        );
92
93 42
        return $this;
94
    }
95
96
    /**
97
     * @return array|string
98
     */
99 1
    public function getAudience()
100
    {
101 1
        return $this->claims->get(Enum::AUDIENCE);
102
    }
103
104
    /**
105
     * @return array
106
     */
107 25
    public function getClaims(): array
108
    {
109 25
        return $this->claims->toArray();
110
    }
111
112
    /**
113
     * @return string|null
114
     */
115 1
    public function getContentType(): ?string
116
    {
117
        /** @var string $result */
118 1
        $result = $this->jose->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->jose->get(Phalcon...T_TYPE, null, 'string') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
119 1
            Enum::CONTENT_TYPE,
120 1
            null,
121 1
            "string"
122
        );
123
124 1
        return $result;
125
    }
126
127
    /**
128
     * @return int|null
129
     */
130 1
    public function getExpirationTime(): ?int
131
    {
132
        /** @var int|null $result */
133 1
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...TION_TIME, null, 'int') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
134 1
            Enum::EXPIRATION_TIME,
135 1
            null,
136 1
            "int"
137
        );
138
139 1
        return $result;
140
    }
141
142
    /**
143
     * @return array
144
     */
145 25
    public function getHeaders(): array
146
    {
147 25
        return $this->jose->toArray();
148
    }
149
150
    /**
151
     * @return string|null
152
     */
153 1
    public function getId(): ?string
154
    {
155
        /** @var string $result */
156 1
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...um::ID, null, 'string') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
157 1
            Enum::ID,
158 1
            null,
159 1
            "string"
160
        );
161
162 1
        return $result;
163
    }
164
165
    /**
166
     * @return int|null
167
     */
168 1
    public function getIssuedAt(): ?int
169
    {
170
        /** @var int|null $result */
171 1
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...ISSUED_AT, null, 'int') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
172 1
            Enum::ISSUED_AT,
173 1
            null,
174 1
            "int"
175
        );
176
177 1
        return $result;
178
    }
179
180
    /**
181
     * @return string|null
182
     */
183 1
    public function getIssuer(): ?string
184
    {
185
        /** @var string|null $result */
186 1
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...ISSUER, null, 'string') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
187 1
            Enum::ISSUER,
188 1
            null,
189 1
            "string"
190
        );
191
192 1
        return $result;
193
    }
194
195
    /**
196
     * @return int|null
197
     */
198 1
    public function getNotBefore(): ?int
199
    {
200
        /** @var int|null $result */
201 1
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...OT_BEFORE, null, 'int') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
202 1
            Enum::NOT_BEFORE,
203 1
            null,
204 1
            "int"
205
        );
206
207 1
        return $result;
208
    }
209
210
    /**
211
     * @return string|null
212
     */
213 2
    public function getSubject(): ?string
214
    {
215
        /** @var string|null $result */
216 2
        $result = $this->claims->get(
0 ignored issues
show
Are you sure the assignment to $result is correct as $this->claims->get(Phalc...UBJECT, null, 'string') targeting Phalcon\Collection::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
217 2
            Enum::SUBJECT,
218 2
            null,
219 2
            "string"
220
        );
221
222 2
        return $result;
223
    }
224
225
    /**
226
     * @return Token
227
     * @throws ValidatorException
228
     */
229 25
    public function getToken(): Token
230
    {
231 25
        if (empty($this->passphrase)) {
232 1
            throw new ValidatorException(
233 1
                "Invalid passphrase (empty)"
234
            );
235
        }
236
237 24
        $encodedClaims    = Base64::encodeUrl(Json::encode($this->getClaims()));
238 24
        $claims           = new Item($this->getClaims(), $encodedClaims);
239 24
        $encodedHeaders   = Base64::encodeUrl(Json::encode($this->getHeaders()));
240 24
        $headers          = new Item($this->getHeaders(), $encodedHeaders);
241 24
        $signatureHash    = $this->signer->sign(
242 24
            $encodedHeaders . "." . $encodedClaims,
243 24
            $this->passphrase
244
        );
245 24
        $encodedSignature = Base64::encodeUrl($signatureHash);
246 24
        $signature        = new Signature($signatureHash, $encodedSignature);
247
248 24
        return new Token($headers, $claims, $signature);
249
    }
250
251
    /**
252
     * @return string
253
     */
254 1
    public function getPassphrase(): string
255
    {
256 1
        return $this->passphrase;
257
    }
258
259
    /**
260
     * The "aud" (audience) claim identifies the recipients that the JWT is
261
     * intended for.  Each principal intended to process the JWT MUST
262
     * identify itself with a value in the audience claim.  If the principal
263
     * processing the claim does not identify itself with a value in the
264
     * "aud" claim when this claim is present, then the JWT MUST be
265
     * rejected.  In the general case, the "aud" value is an array of case-
266
     * sensitive strings, each containing a StringOrURI value.  In the
267
     * special case when the JWT has one audience, the "aud" value MAY be a
268
     * single case-sensitive string containing a StringOrURI value.  The
269
     * interpretation of audience values is generally application specific.
270
     * Use of this claim is OPTIONAL.
271
     *
272
     * @param mixed $audience
273
     *
274
     * @return Builder
275
     * @throws ValidatorException
276
     */
277 26
    public function setAudience($audience): Builder
278
    {
279 26
        if (!is_string($audience) && !is_array($audience)) {
280 1
            throw new ValidatorException(
281 1
                "Invalid Audience"
282
            );
283
        }
284
285 25
        if (is_string($audience)) {
286 25
            $audience = [$audience];
287
        }
288
289 25
        return $this->setClaim(Enum::AUDIENCE, $audience);
290
    }
291
292
    /**
293
     * Sets the content type header 'cty'
294
     *
295
     * @param string $contentType
296
     *
297
     * @return Builder
298
     */
299 1
    public function setContentType(string $contentType): Builder
300
    {
301 1
        $this->jose->set(Enum::CONTENT_TYPE, $contentType);
302
303 1
        return $this;
304
    }
305
306
    /**
307
     * The "exp" (expiration time) claim identifies the expiration time on
308
     * or after which the JWT MUST NOT be accepted for processing.  The
309
     * processing of the "exp" claim requires that the current date/time
310
     * MUST be before the expiration date/time listed in the "exp" claim.
311
     * Implementers MAY provide for some small leeway, usually no more than
312
     * a few minutes, to account for clock skew.  Its value MUST be a number
313
     * containing a NumericDate value.  Use of this claim is OPTIONAL.
314
     *
315
     * @param int $timestamp
316
     *
317
     * @return Builder
318
     * @throws ValidatorException
319
     */
320 26
    public function setExpirationTime(int $timestamp): Builder
321
    {
322 26
        if ($timestamp < time()) {
323 1
            throw new ValidatorException(
324 1
                "Invalid Expiration Time"
325
            );
326
        }
327
328 25
        return $this->setClaim(Enum::EXPIRATION_TIME, $timestamp);
329
    }
330
331
    /**
332
     * The "jti" (JWT ID) claim provides a unique identifier for the JWT.
333
     * The identifier value MUST be assigned in a manner that ensures that
334
     * there is a negligible probability that the same value will be
335
     * accidentally assigned to a different data object; if the application
336
     * uses multiple issuers, collisions MUST be prevented among values
337
     * produced by different issuers as well.  The "jti" claim can be used
338
     * to prevent the JWT from being replayed.  The "jti" value is a case-
339
     * sensitive string.  Use of this claim is OPTIONAL.
340
     *
341
     * @param string $id
342
     *
343
     * @return Builder
344
     */
345 25
    public function setId(string $id): Builder
346
    {
347 25
        return $this->setClaim(Enum::ID, $id);
348
    }
349
350
    /**
351
     * The "iat" (issued at) claim identifies the time at which the JWT was
352
     * issued.  This claim can be used to determine the age of the JWT.  Its
353
     * value MUST be a number containing a NumericDate value.  Use of this
354
     * claim is OPTIONAL.
355
     *
356
     * @param int $timestamp
357
     *
358
     * @return Builder
359
     */
360 25
    public function setIssuedAt(int $timestamp): Builder
361
    {
362 25
        return $this->setClaim(Enum::ISSUED_AT, $timestamp);
363
    }
364
365
    /**
366
     * The "iss" (issuer) claim identifies the principal that issued the
367
     * JWT.  The processing of this claim is generally application specific.
368
     * The "iss" value is a case-sensitive string containing a StringOrURI
369
     * value.  Use of this claim is OPTIONAL.
370
     *
371
     * @param string $issuer
372
     *
373
     * @return Builder
374
     */
375 25
    public function setIssuer(string $issuer): Builder
376
    {
377 25
        return $this->setClaim(Enum::ISSUER, $issuer);
378
    }
379
380
    /**
381
     * The "nbf" (not before) claim identifies the time before which the JWT
382
     * MUST NOT be accepted for processing.  The processing of the "nbf"
383
     * claim requires that the current date/time MUST be after or equal to
384
     * the not-before date/time listed in the "nbf" claim.  Implementers MAY
385
     * provide for some small leeway, usually no more than a few minutes, to
386
     * account for clock skew.  Its value MUST be a number containing a
387
     * NumericDate value.  Use of this claim is OPTIONAL.
388
     *
389
     * @param int $timestamp
390
     *
391
     * @return Builder
392
     * @throws ValidatorException
393
     */
394 26
    public function setNotBefore(int $timestamp): Builder
395
    {
396 26
        if ($timestamp > time()) {
397 1
            throw new ValidatorException(
398 1
                "Invalid Not Before"
399
            );
400
        }
401
402 25
        return $this->setClaim(Enum::NOT_BEFORE, $timestamp);
403
    }
404
405
    /**
406
     * The "sub" (subject) claim identifies the principal that is the
407
     * subject of the JWT.  The claims in a JWT are normally statements
408
     * about the subject.  The subject value MUST either be scoped to be
409
     * locally unique in the context of the issuer or be globally unique.
410
     * The processing of this claim is generally application specific.  The
411
     * "sub" value is a case-sensitive string containing a StringOrURI
412
     * value.  Use of this claim is OPTIONAL.
413
     *
414
     * @param string $subject
415
     *
416
     * @return Builder
417
     */
418 26
    public function setSubject(string $subject): Builder
419
    {
420 26
        return $this->setClaim(Enum::SUBJECT, $subject);
421
    }
422
423
    /**
424
     * @param string $passphrase
425
     *
426
     * @return Builder
427
     * @throws ValidatorException
428
     */
429 26
    public function setPassphrase(string $passphrase): Builder
430
    {
431
        if (
432 26
            !preg_match(
433 26
                "/^.*(?=.{12,}+)(?=.*[0-9]+)(?=.*[A-Z]+)(?=.*[a-z]+)(?=.*[*&!@%^#\$]+).*$/",
434 26
                $passphrase
435
            )
436
        ) {
437 1
            throw new ValidatorException(
438 1
                "Invalid passphrase (too weak)"
439
            );
440
        }
441
442 25
        $this->passphrase = $passphrase;
443
444 25
        return $this;
445
    }
446
447
    /**
448
     * Sets a registered claim
449
     *
450
     * @param string $name
451
     * @param mixed  $value
452
     *
453
     * @return Builder
454
     */
455 32
    private function setClaim(string $name, $value): Builder
456
    {
457 32
        $this->claims->set($name, $value);
458
459 32
        return $this;
460
    }
461
}
462