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) { |
|
|
|
|
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()) { |
|
|
|
|
174
|
|
|
throw new InvalidTimeException("Token is expired by 'exp'"); |
175
|
|
|
} |
176
|
|
View Code Duplication |
if (isset($claims['iat']) && $claims['iat'] < ($this->minIssueTime + $this->issuerTimeLeeway)) { |
|
|
|
|
177
|
|
|
throw new InvalidTimeException("Server deemed your token too old"); |
178
|
|
|
} |
179
|
|
View Code Duplication |
if (isset($claims['nbf']) && ($claims['nbf'] - $this->issuerTimeLeeway) > time()) { |
|
|
|
|
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
|
|
|
|
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.