1 | <?php |
||||
2 | /** |
||||
3 | * This file is part of the O2System Framework 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 | * @author Steeve Andrian Salim |
||||
9 | * @copyright Copyright (c) Steeve Andrian Salim |
||||
10 | */ |
||||
11 | |||||
12 | // ------------------------------------------------------------------------ |
||||
13 | |||||
14 | namespace O2System\Security\Generators; |
||||
15 | |||||
16 | // ------------------------------------------------------------------------ |
||||
17 | |||||
18 | use O2System\Security\Encoders\Base64; |
||||
19 | use O2System\Security\Encoders\Json; |
||||
20 | use O2System\Security\Encryptions\Algorithm; |
||||
21 | use O2System\Spl\Traits\Collectors\ErrorCollectorTrait; |
||||
22 | |||||
23 | /** |
||||
24 | * Class Token |
||||
25 | * |
||||
26 | * Security token generator. |
||||
27 | * |
||||
28 | * @package O2System\Security\Generators |
||||
29 | */ |
||||
30 | class Token |
||||
31 | { |
||||
32 | use ErrorCollectorTrait; |
||||
33 | |||||
34 | /** |
||||
35 | * Token::ALPHANUMERIC_STRING |
||||
36 | * |
||||
37 | * @var int |
||||
38 | */ |
||||
39 | const ALPHANUMERIC_STRING = 0; |
||||
40 | |||||
41 | /** |
||||
42 | * Token::ALPHAUPPERCASE_STRING |
||||
43 | * |
||||
44 | * @var int |
||||
45 | */ |
||||
46 | const ALPHAUPPERCASE_STRING = 1; |
||||
47 | |||||
48 | /** |
||||
49 | * Token::ALPHALOWERCASE_STRING |
||||
50 | * |
||||
51 | * @var int |
||||
52 | */ |
||||
53 | const ALPHALOWERCASE_STRING = 2; |
||||
54 | |||||
55 | /** |
||||
56 | * Token::ALPHAHASH_STRING |
||||
57 | * |
||||
58 | * @var int |
||||
59 | */ |
||||
60 | const ALPHAHASH_STRING = 3; |
||||
61 | |||||
62 | /** |
||||
63 | * Token::NUMERIC_STRING |
||||
64 | * |
||||
65 | * @var int |
||||
66 | */ |
||||
67 | const NUMERIC_STRING = 4; |
||||
68 | |||||
69 | /** |
||||
70 | * Token::$key |
||||
71 | * |
||||
72 | * @var string|null |
||||
73 | */ |
||||
74 | protected $key = null; |
||||
75 | |||||
76 | /** |
||||
77 | * Allow the current timestamp to be specified. |
||||
78 | * Useful for fixing a value within unit testing. |
||||
79 | * |
||||
80 | * Will default to PHP time() value if null. |
||||
81 | */ |
||||
82 | protected $timestamp; |
||||
83 | |||||
84 | /** |
||||
85 | * Token::$algorithm |
||||
86 | * |
||||
87 | * @var string |
||||
88 | */ |
||||
89 | protected $algorithm = 'HMAC-SHA256'; |
||||
90 | |||||
91 | /** |
||||
92 | * Token::$headers |
||||
93 | * |
||||
94 | * @var array |
||||
95 | */ |
||||
96 | protected $headers = []; |
||||
97 | |||||
98 | // ------------------------------------------------------------------------ |
||||
99 | |||||
100 | /** |
||||
101 | * Token::__construct |
||||
102 | */ |
||||
103 | public function __construct() |
||||
104 | { |
||||
105 | if (class_exists('O2System\Framework', false)) { |
||||
106 | $this->key = config()->getItem('security')->encryptionKey; |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
107 | } |
||||
108 | } |
||||
109 | |||||
110 | // ------------------------------------------------------------------------ |
||||
111 | |||||
112 | /** |
||||
113 | * Token::generate |
||||
114 | * |
||||
115 | * @param int $length Token string length. |
||||
116 | * @param int $type Token string type. |
||||
117 | * |
||||
118 | * @return string |
||||
119 | * @throws \Exception |
||||
120 | */ |
||||
121 | public static function generate($length = 8, $type = self::ALPHANUMERIC_STRING) |
||||
122 | { |
||||
123 | if ($type !== self::ALPHAHASH_STRING) { |
||||
124 | switch ($type) { |
||||
125 | default: |
||||
126 | case self::ALPHANUMERIC_STRING: |
||||
127 | $codeAlphabet = implode(range('A', 'Z')); // Uppercase Alphabet |
||||
128 | $codeAlphabet .= implode(range('a', 'z')); // Lowercase Alphabet |
||||
129 | $codeAlphabet .= implode(range(0, 9)); // Numeric Alphabet |
||||
130 | break; |
||||
131 | case self::ALPHAUPPERCASE_STRING: |
||||
132 | $codeAlphabet = implode(range('A', 'Z')); // Uppercase Alphabet |
||||
133 | break; |
||||
134 | case self::ALPHALOWERCASE_STRING: |
||||
135 | $codeAlphabet = implode(range('a', 'z')); // Lowercase Alphabet |
||||
136 | break; |
||||
137 | case self::NUMERIC_STRING: |
||||
138 | $codeAlphabet = implode(range(0, 9)); // Numeric Alphabet |
||||
139 | break; |
||||
140 | } |
||||
141 | |||||
142 | $token = ''; |
||||
143 | $max = strlen($codeAlphabet); |
||||
144 | |||||
145 | for ($i = 0; $i < $length; $i++) { |
||||
146 | $token .= $codeAlphabet[ random_int(0, $max - 1) ]; |
||||
147 | } |
||||
148 | |||||
149 | return $token; |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * ALPHABIN2HEX_STRING |
||||
154 | */ |
||||
155 | if (function_exists('random_bytes')) { |
||||
156 | $randomData = random_bytes(20); |
||||
157 | if ($randomData !== false && strlen($randomData) === 20) { |
||||
158 | return bin2hex($randomData); |
||||
159 | } |
||||
160 | } |
||||
161 | if (function_exists('openssl_random_pseudo_bytes')) { |
||||
162 | $randomData = openssl_random_pseudo_bytes(20); |
||||
163 | if ($randomData !== false && strlen($randomData) === 20) { |
||||
164 | return bin2hex($randomData); |
||||
165 | } |
||||
166 | } |
||||
167 | if (function_exists('mcrypt_create_iv')) { |
||||
168 | $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); |
||||
0 ignored issues
–
show
The function
mcrypt_create_iv() has been deprecated: 7.1
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
169 | if ($randomData !== false && strlen($randomData) === 20) { |
||||
170 | return bin2hex($randomData); |
||||
171 | } |
||||
172 | } |
||||
173 | if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data |
||||
174 | $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); |
||||
175 | if ($randomData !== false && strlen($randomData) === 20) { |
||||
176 | return bin2hex($randomData); |
||||
177 | } |
||||
178 | } |
||||
179 | // Last resort which you probably should just get rid of: |
||||
180 | $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); |
||||
181 | |||||
182 | return substr(hash('sha512', $randomData), 0, $length); |
||||
183 | } |
||||
184 | |||||
185 | // ------------------------------------------------------------------------ |
||||
186 | |||||
187 | /** |
||||
188 | * Token::setKey |
||||
189 | * |
||||
190 | * @param string $key |
||||
191 | * |
||||
192 | * @return static |
||||
193 | */ |
||||
194 | public function setKey($key) |
||||
195 | { |
||||
196 | $this->key = $key; |
||||
197 | |||||
198 | return $this; |
||||
199 | } |
||||
200 | |||||
201 | // ------------------------------------------------------------------------ |
||||
202 | |||||
203 | /** |
||||
204 | * Token::setAlgorithm |
||||
205 | * |
||||
206 | * @param string $algorithm |
||||
207 | * |
||||
208 | * @return static |
||||
209 | */ |
||||
210 | public function setAlgorithm($algorithm) |
||||
211 | { |
||||
212 | $algorithm = strtoupper($algorithm); |
||||
213 | |||||
214 | if (Algorithm::validate($algorithm)) { |
||||
215 | $this->algorithm = $algorithm; |
||||
216 | } |
||||
217 | |||||
218 | return $this; |
||||
219 | } |
||||
220 | |||||
221 | // ------------------------------------------------------------------------ |
||||
222 | |||||
223 | /** |
||||
224 | * Token::setTimestamp |
||||
225 | * |
||||
226 | * @param int|string $timestamp |
||||
227 | * |
||||
228 | * @return static |
||||
229 | */ |
||||
230 | public function setTimestamp($timestamp) |
||||
231 | { |
||||
232 | $this->timestamp = is_numeric($timestamp) ? $timestamp : strtotime($timestamp); |
||||
233 | |||||
234 | return $this; |
||||
235 | } |
||||
236 | |||||
237 | // ------------------------------------------------------------------------ |
||||
238 | |||||
239 | /** |
||||
240 | * Token::encode |
||||
241 | * |
||||
242 | * @param array $payload |
||||
243 | * @param null $key |
||||
0 ignored issues
–
show
|
|||||
244 | * |
||||
245 | * @return string |
||||
246 | * @throws \O2System\Spl\Exceptions\Logic\DomainException |
||||
247 | */ |
||||
248 | public function encode(array $payload, $key = null) |
||||
249 | { |
||||
250 | $key = empty($key) ? $this->key : $key; |
||||
251 | |||||
252 | $this->addHeader('algorithm', $this->algorithm); |
||||
253 | |||||
254 | // Create Header Segment |
||||
255 | $segments[] = Base64::encode(Json::encode($this->headers)); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
256 | |||||
257 | // Create Payload Segment |
||||
258 | $segments[] = Base64::encode(Json::encode($payload)); |
||||
259 | |||||
260 | // Create Signature Segment |
||||
261 | $segments[] = Base64::encode(Signature::generate($segments, $key, $this->algorithm)); |
||||
262 | |||||
263 | return implode('.', $segments); |
||||
264 | } |
||||
265 | |||||
266 | // ------------------------------------------------------------------------ |
||||
267 | |||||
268 | /** |
||||
269 | * Token::addHeader |
||||
270 | * |
||||
271 | * @param string $key |
||||
272 | * @param mixed $value |
||||
273 | * |
||||
274 | * @return static |
||||
275 | */ |
||||
276 | public function addHeader($key, $value) |
||||
277 | { |
||||
278 | $this->headers[ $key ] = $value; |
||||
279 | |||||
280 | return $this; |
||||
281 | } |
||||
282 | |||||
283 | // ------------------------------------------------------------------------ |
||||
284 | |||||
285 | /** |
||||
286 | * Token::decode |
||||
287 | * |
||||
288 | * @param string $token |
||||
289 | * @param null $key |
||||
0 ignored issues
–
show
|
|||||
290 | * |
||||
291 | * @return bool|\O2System\Spl\DataStructures\SplArrayObject|string|null |
||||
292 | */ |
||||
293 | public function decode($token, $key = null) |
||||
294 | { |
||||
295 | $key = empty($key) ? $this->key : $key; |
||||
296 | |||||
297 | $timestamp = empty($this->timestamp) ? time() : $this->timestamp; |
||||
298 | |||||
299 | $segments = explode('.', $token); |
||||
300 | $segments = array_map('trim', $segments); |
||||
301 | |||||
302 | if (count($segments) == 3) { |
||||
303 | list($headers, $payload, $signature) = $segments; |
||||
304 | |||||
305 | // Base64 decode headers |
||||
306 | if (false === ($headers = Base64::decode($headers))) { |
||||
307 | $this->errors[] = 'Invalid header base64 decoding'; |
||||
308 | |||||
309 | return false; |
||||
310 | } |
||||
311 | |||||
312 | // Json decode headers |
||||
313 | if (null === ($headers = Json::decode($headers))) { |
||||
314 | $this->errors[] = 'Invalid header json decoding'; |
||||
315 | |||||
316 | return false; |
||||
317 | } |
||||
318 | |||||
319 | // Validate algorithm header |
||||
320 | if (empty($headers->alg)) { |
||||
321 | $this->errors[] = 'Invalid algorithm'; |
||||
322 | |||||
323 | return false; |
||||
324 | } elseif ( ! Algorithm::validate($headers->alg)) { |
||||
325 | $this->errors[] = 'Unsupported algorithm'; |
||||
326 | |||||
327 | return false; |
||||
328 | } |
||||
329 | |||||
330 | // Base64 decode payload |
||||
331 | if (false === ($payload = Base64::decode($payload))) { |
||||
332 | $this->errors[] = 'Invalid payload base64 decoding'; |
||||
333 | |||||
334 | return false; |
||||
335 | } |
||||
336 | |||||
337 | // Json decode payload |
||||
338 | if (null === ($payload = Json::decode($payload))) { |
||||
339 | $this->errors[] = 'Invalid payload json decoding'; |
||||
340 | |||||
341 | return false; |
||||
342 | } |
||||
343 | |||||
344 | // Base64 decode payload |
||||
345 | if (false === ($signature = Base64::decode($signature))) { |
||||
346 | $this->errors[] = 'Invalid signature base64 decoding'; |
||||
347 | |||||
348 | return false; |
||||
349 | } |
||||
350 | |||||
351 | if (Signature::verify($token, $signature, $key, $headers->alg) === false) { |
||||
352 | $this->errors[] = 'Invalid signature'; |
||||
353 | |||||
354 | return false; |
||||
355 | } |
||||
356 | |||||
357 | // Check if the nbf if it is defined. This is the time that the |
||||
358 | // token can actually be used. If it's not yet that time, abort. |
||||
359 | if (isset($payload->nbf) && $payload->nbf > $timestamp) { |
||||
360 | $this->errors[] = 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->nbf); |
||||
361 | |||||
362 | return false; |
||||
363 | } |
||||
364 | |||||
365 | // Check that this token has been created before 'now'. This prevents |
||||
366 | // using tokens that have been created for later use (and haven't |
||||
367 | // correctly used the nbf claim). |
||||
368 | if (isset($payload->iat) && $payload->iat > $timestamp) { |
||||
369 | $this->errors[] = 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->iat); |
||||
370 | |||||
371 | return false; |
||||
372 | } |
||||
373 | // Check if this token has expired. |
||||
374 | if (isset($payload->exp) && $timestamp >= $payload->exp) { |
||||
375 | $this->errors[] = 'Expired token'; |
||||
376 | |||||
377 | return false; |
||||
378 | } |
||||
379 | |||||
380 | return $payload; |
||||
381 | } |
||||
382 | |||||
383 | return false; |
||||
384 | } |
||||
385 | } |