Completed
Push — master ( ff92f6...3ef1d3 )
by John
09:08
created

JwtKey::validateClaims()   D

Complexity

Conditions 27
Paths 23

Size

Total Lines 44
Code Lines 26

Duplication

Lines 9
Ratio 20.45 %

Importance

Changes 0
Metric Value
dl 9
loc 44
rs 4.6752
c 0
b 0
f 0
cc 27
eloc 26
nc 23
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types = 1);
2
/*
3
 * This file is part of the KleijnWeb\JwtBundle package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace KleijnWeb\JwtBundle\Jwt;
10
11
use KleijnWeb\JwtBundle\Jwt\Exception\InvalidTimeException;
12
use KleijnWeb\JwtBundle\Jwt\Exception\KeyTokenMismatchException;
13
use KleijnWeb\JwtBundle\Jwt\Exception\MissingClaimsException;
14
use KleijnWeb\JwtBundle\Jwt\SignatureValidator\HmacValidator;
15
use KleijnWeb\JwtBundle\Jwt\SignatureValidator\RsaValidator;
16
use KleijnWeb\JwtBundle\Jwt\SignatureValidator\SignatureValidator;
17
18
/**
19
 * @author John Kleijn <[email protected]>
20
 */
21
class JwtKey
22
{
23
    const TYPE_HMAC = 'HS256';
24
    const TYPE_RSA  = 'RS256';
25
26
    /**
27
     * @var string
28
     */
29
    private $id;
30
31
    /**
32
     * @var string
33
     */
34
    private $issuer;
35
36
    /**
37
     * @var string
38
     */
39
    private $type = self::TYPE_HMAC;
40
41
    /**
42
     * @var array
43
     */
44
    private $audience = [];
45
46
    /**
47
     * @var int
48
     */
49
    private $minIssueTime;
50
51
    /**
52
     * @var array
53
     */
54
    private $requiredClaims = [];
55
56
    /**
57
     * @var int
58
     */
59
    private $issuerTimeLeeway;
60
61
    /**
62
     * @var string
63
     */
64
    private $secret;
65
66
    /**
67
     * @var SecretLoader
68
     */
69
    private $secretLoader;
70
71
    /**
72
     * @param array $options
73
     */
74
    public function __construct(array $options)
75
    {
76
        if (!isset($options['secret']) && !isset($options['loader'])) {
77
            throw new \InvalidArgumentException("Need a secret or a loader to verify tokens");
78
        }
79
        if (isset($options['secret']) && isset($options['loader'])) {
80
            throw new \InvalidArgumentException("Cannot configure both secret and loader");
81
        }
82
        $defaults = [
83
            'kid'          => null,
84
            'issuer'       => null,
85
            'audience'     => [],
86
            'minIssueTime' => null,
87
            'leeway'       => 0,
88
            'type'         => $this->type,
89
            'require'      => $this->requiredClaims,
90
        ];
91
92
        $options                = array_merge($defaults, $options);
93
        $this->issuer           = $options['issuer'];
94
        $this->audience         = $options['audience'];
95
        $this->type             = $options['type'];
96
        $this->minIssueTime     = $options['minIssueTime'];
97
        $this->requiredClaims   = $options['require'];
98
        $this->issuerTimeLeeway = $options['leeway'];
99
        $this->id               = $options['kid'];
100
        $this->secret           = isset($options['secret']) ? $options['secret'] : null;
101
        $this->secretLoader     = isset($options['loader']) ? $options['loader'] : null;
102
    }
103
104
    /**
105
     * @return string
106
     */
107
    public function getId(): string
108
    {
109
        return $this->id;
110
    }
111
112
    /**
113
     * @param JwtToken $token
114
     *
115
     * @throws \InvalidArgumentException
116
     */
117
    public function validateToken(JwtToken $token)
118
    {
119
        $this->validateHeader($token->getHeader());
120
        $this->validateClaims($token->getClaims());
121
122
        if (!$this->secretLoader) {
123
            $token->validateSignature($this->secret, $this->getSignatureValidator());
124
125
            return;
126
        }
127
        $token->validateSignature($this->secretLoader->load($token), $this->getSignatureValidator());
128
    }
129
130
    /**
131
     * @param array $header
132
     *
133
     * @throws \InvalidArgumentException
134
     */
135
    public function validateHeader(array $header)
136
    {
137
        if (!isset($header['alg'])) {
138
            throw new \InvalidArgumentException("Missing 'alg' in header");
139
        }
140
        if (!isset($header['typ'])) {
141
            throw new \InvalidArgumentException("Missing 'typ' in header");
142
        }
143
        if ($this->type !== $header['alg']) {
144
            throw new \InvalidArgumentException("Algorithm mismatch");
145
        }
146
    }
147
148
    /**
149
     * @param array $claims
150
     *
151
     * @throws \InvalidArgumentException
152
     */
153
    public function validateClaims(array $claims)
154
    {
155
        if ($this->requiredClaims) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->requiredClaims of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
156
            $missing = array_diff_key(array_flip($this->requiredClaims), $claims);
157
            if (count($missing)) {
158
                throw new MissingClaimsException("Missing claims: " . implode(', ', $missing));
159
            }
160
        }
161
        if ($this->issuer && !isset($claims['iss'])) {
162
            throw new MissingClaimsException("Claim 'iss' is required");
163
        }
164
        if ($this->minIssueTime && !isset($claims['iat'])) {
165
            throw new MissingClaimsException("Claim 'iat' is required");
166
        }
167
        if (!empty($this->audience) && !isset($claims['aud'])) {
168
            throw new MissingClaimsException("Claim 'aud' is required");
169
        }
170
        if ((!isset($claims['sub']) || empty($claims['sub'])) && (!isset($claims['prn']) || empty($claims['prn']))) {
171
            throw new MissingClaimsException("Missing principle subject claim");
172
        }
173 View Code Duplication
        if (isset($claims['exp']) && $claims['exp'] + $this->issuerTimeLeeway < time()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
            throw new InvalidTimeException("Token is expired by 'exp'");
175
        }
176 View Code Duplication
        if (isset($claims['iat']) && $claims['iat'] < ($this->minIssueTime + $this->issuerTimeLeeway)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
            throw new InvalidTimeException("Server deemed your token too old");
178
        }
179 View Code Duplication
        if (isset($claims['nbf']) && ($claims['nbf'] - $this->issuerTimeLeeway) > time()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
180
            throw new InvalidTimeException("Token not valid yet");
181
        }
182
        if (isset($claims['iss']) && $claims['iss'] !== $this->issuer) {
183
            throw new KeyTokenMismatchException("Issuer mismatch");
184
        }
185
186
        if (count($this->audience)) {
187
            if (isset($claims['aud']) &&
188
                (
189
                    (is_array($this->audience) && !in_array($claims['aud'], $this->audience))
190
                    || (!is_array($this->audience) && $claims['aud'] !== $this->audience)
191
                )
192
            ) {
193
                throw new KeyTokenMismatchException("Audience mismatch");
194
            }
195
        }
196
    }
197
198
199
    /**
200
     * @return SignatureValidator
201
     */
202
    public function getSignatureValidator(): SignatureValidator
203
    {
204
        if ($this->type == self::TYPE_RSA) {
205
            return new RsaValidator();
206
        }
207
208
        return new HmacValidator();
209
    }
210
211
    /**
212
     * Prevent accidental persistence of secret
213
     */
214
    final public function __sleep()
215
    {
216
        return [];
217
    }
218
}
219