1
|
|
|
<?php |
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\Authenticator; |
10
|
|
|
|
11
|
|
|
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\SignatureValidator; |
12
|
|
|
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\HmacValidator; |
13
|
|
|
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\RsaValidator; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* @author John Kleijn <[email protected]> |
17
|
|
|
*/ |
18
|
|
|
class JwtKey |
19
|
|
|
{ |
20
|
|
|
const TYPE_HMAC = 'HS256'; |
21
|
|
|
const TYPE_RSA = 'RS256'; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var string |
25
|
|
|
*/ |
26
|
|
|
private $id; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
private $issuer; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
private $type = self::TYPE_HMAC; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var string |
40
|
|
|
*/ |
41
|
|
|
private $audience; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var int |
45
|
|
|
*/ |
46
|
|
|
private $minIssueTime; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
private $requiredClaims = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var int |
55
|
|
|
*/ |
56
|
|
|
private $issuerTimeLeeway; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
private $secret; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var SecretLoader |
65
|
|
|
*/ |
66
|
|
|
private $secretLoader; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @param array $options |
70
|
|
|
*/ |
71
|
|
|
public function __construct(array $options) |
72
|
|
|
{ |
73
|
|
|
if (!isset($options['secret']) && !isset($options['loader'])) { |
74
|
|
|
throw new \InvalidArgumentException("Need a secret or a loader to verify tokens"); |
75
|
|
|
} |
76
|
|
|
if (isset($options['secret']) && isset($options['loader'])) { |
77
|
|
|
throw new \InvalidArgumentException("Cannot configure both secret and loader"); |
78
|
|
|
} |
79
|
|
|
$defaults = [ |
80
|
|
|
'kid' => null, |
81
|
|
|
'issuer' => null, |
82
|
|
|
'audience' => null, |
83
|
|
|
'minIssueTime' => null, |
84
|
|
|
'leeway' => 0, |
85
|
|
|
'type' => $this->type, |
86
|
|
|
'require' => $this->requiredClaims, |
87
|
|
|
]; |
88
|
|
|
|
89
|
|
|
$options = array_merge($defaults, $options); |
90
|
|
|
$this->issuer = $options['issuer']; |
91
|
|
|
$this->audience = $options['audience']; |
92
|
|
|
$this->type = $options['type']; |
93
|
|
|
$this->minIssueTime = $options['minIssueTime']; |
94
|
|
|
$this->requiredClaims = $options['require']; |
95
|
|
|
$this->issuerTimeLeeway = $options['leeway']; |
96
|
|
|
$this->id = $options['kid']; |
97
|
|
|
$this->secret = isset($options['secret']) ? $options['secret'] : null; |
98
|
|
|
$this->secretLoader = isset($options['loader']) ? $options['loader'] : null; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @return string |
103
|
|
|
*/ |
104
|
|
|
public function getId() |
105
|
|
|
{ |
106
|
|
|
return $this->id; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @param JwtToken $token |
111
|
|
|
* |
112
|
|
|
* @throws \InvalidArgumentException |
113
|
|
|
*/ |
114
|
|
|
public function validateToken(JwtToken $token) |
115
|
|
|
{ |
116
|
|
|
$this->validateHeader($token->getHeader()); |
117
|
|
|
$this->validateClaims($token->getClaims()); |
118
|
|
|
|
119
|
|
|
if (!$this->secretLoader) { |
120
|
|
|
$token->validateSignature($this->secret, $this->getSignatureValidator()); |
121
|
|
|
return; |
122
|
|
|
} |
123
|
|
|
$token->validateSignature($this->secretLoader->load($token), $this->getSignatureValidator()); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @param array $header |
128
|
|
|
* |
129
|
|
|
* @throws \InvalidArgumentException |
130
|
|
|
*/ |
131
|
|
|
public function validateHeader(array $header) |
132
|
|
|
{ |
133
|
|
|
if (!isset($header['alg'])) { |
134
|
|
|
throw new \InvalidArgumentException("Missing 'alg' in header"); |
135
|
|
|
} |
136
|
|
|
if (!isset($header['typ'])) { |
137
|
|
|
throw new \InvalidArgumentException("Missing 'typ' in header"); |
138
|
|
|
} |
139
|
|
|
if ($this->type !== $header['alg']) { |
140
|
|
|
throw new \InvalidArgumentException("Algorithm mismatch"); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @param array $claims |
146
|
|
|
* |
147
|
|
|
* @throws \InvalidArgumentException |
148
|
|
|
*/ |
149
|
|
|
public function validateClaims(array $claims) |
150
|
|
|
{ |
151
|
|
|
if ($this->requiredClaims) { |
|
|
|
|
152
|
|
|
$missing = array_diff_key(array_flip($this->requiredClaims), $claims); |
153
|
|
|
if (count($missing)) { |
154
|
|
|
throw new \InvalidArgumentException("Missing claims: " . implode(', ', $missing)); |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
if ($this->issuer && !isset($claims['iss'])) { |
158
|
|
|
throw new \InvalidArgumentException("Claim 'iss' is required"); |
159
|
|
|
} |
160
|
|
|
if ($this->minIssueTime && !isset($claims['iat'])) { |
161
|
|
|
throw new \InvalidArgumentException("Claim 'iat' is required"); |
162
|
|
|
} |
163
|
|
|
if ($this->audience && !isset($claims['aud'])) { |
164
|
|
|
throw new \InvalidArgumentException("Claim 'aud' is required"); |
165
|
|
|
} |
166
|
|
|
if (!isset($claims['prn']) || empty($claims['prn'])) { |
167
|
|
|
throw new \InvalidArgumentException("Missing principle claim"); |
168
|
|
|
} |
169
|
|
View Code Duplication |
if (isset($claims['exp']) && $claims['exp'] < time()) { |
|
|
|
|
170
|
|
|
throw new \InvalidArgumentException("Token is expired by 'exp'"); |
171
|
|
|
} |
172
|
|
View Code Duplication |
if (isset($claims['iat']) && $claims['iat'] < $this->minIssueTime) { |
|
|
|
|
173
|
|
|
throw new \InvalidArgumentException("Server deemed your token too old"); |
174
|
|
|
} |
175
|
|
View Code Duplication |
if (isset($claims['nbf']) && $claims['nbf'] > time()) { |
|
|
|
|
176
|
|
|
throw new \InvalidArgumentException("Token not valid yet"); |
177
|
|
|
} |
178
|
|
View Code Duplication |
if (isset($claims['iss']) && $claims['iss'] !== $this->issuer) { |
|
|
|
|
179
|
|
|
throw new \InvalidArgumentException("Issuer mismatch"); |
180
|
|
|
} |
181
|
|
View Code Duplication |
if (isset($claims['aud']) && $claims['aud'] !== $this->audience) { |
|
|
|
|
182
|
|
|
throw new \InvalidArgumentException("Audience mismatch"); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* @return SignatureValidator |
189
|
|
|
*/ |
190
|
|
|
public function getSignatureValidator() |
191
|
|
|
{ |
192
|
|
|
if ($this->type == self::TYPE_RSA) { |
193
|
|
|
return new RsaValidator(); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return new HmacValidator(); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Prevent accidental persistence of secret |
201
|
|
|
*/ |
202
|
|
|
final public function __sleep() |
203
|
|
|
{ |
204
|
|
|
return []; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
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.