LTI Integration Library 4.10.3
PHP class library for building LTI integrations
 
Loading...
Searching...
No Matches
WebTokenClient.php
1<?php
2
3namespace ceLTIc\LTI\Jwt;
4
5use Jose\Component\Core;
6use Jose\Component\Signature;
7use Jose\Component\Signature\JWS;
8use Jose\Component\KeyManagement;
9use Jose\Component\Checker;
10use Jose\Component\Encryption;
11use Jose\Component\Encryption\JWE;
14
23{
24
28 const SUPPORTED_ALGORITHMS = array('RS256', 'RS384', 'RS512');
29
35 private $jwe = null;
36
42 private $jwt = null;
43
49 private $claims = null;
50
56 private static $lastHeaders = null;
57
63 private static $lastPayload = null;
64
70 public static function getSupportedAlgorithms()
71 {
73 }
74
80 public function hasJwt()
81 {
82 return !empty($this->jwt);
83 }
84
90 public function isEncrypted()
91 {
92 return !empty($this->jwe);
93 }
94
103 public function load($jwtString, $privateKey = null)
104 {
105 $ok = true;
106 $this->jwe = null;
107 $this->jwt = null;
108 $this->claims = null;
109 try {
110 $serializer = new Signature\Serializer\CompactSerializer();
111 $this->jwt = $serializer->unserialize($jwtString);
112 } catch (\Exception $e) {
113 $ok = false;
114 }
115 if (!$ok) {
116 try {
117 $serializer = new Encryption\Serializer\CompactSerializer();
118 $this->jwt = $serializer->unserialize($jwtString);
119 $ok = $this->decrypt($privateKey);
120 } catch (\Exception $e) {
121 $ok = false;
122 }
123 }
124 if ($ok) {
125 $this->claims = Util::jsonDecode($this->jwt->getPayload());
126 }
127
128 return $ok;
129 }
130
136 public function getJweHeaders()
137 {
138 if ($this->isEncrypted()) {
139 $headers = $this->jwe->getSharedProtectedHeader();
140 } else {
141 $headers = array();
142 }
143
144 return $headers;
145 }
146
154 public function hasHeader($name)
155 {
156 if ($this->jwt instanceof Signature\JWS) {
157 $ok = $this->jwt->getSignature(0)->hasProtectedHeaderParameter($name);
158 } else {
159 $ok = false;
160 }
161
162 return $ok;
163 }
164
173 public function getHeader($name, $defaultValue = null)
174 {
175 try {
176 $value = $this->jwt->getSignature(0)->getProtectedHeaderParameter($name);
177 } catch (\Exception $e) {
178 $value = $defaultValue;
179 }
180
181 return $value;
182 }
183
189 public function getHeaders()
190 {
191 $headers = null;
192 if ($this->jwt instanceof Signature\JWS) {
193 $headers = $this->jwt->getSignature(0)->getProtectedHeader();
194 }
195
196 return $headers;
197 }
198
204 public static function getLastHeaders()
205 {
206 return self::$lastHeaders;
207 }
208
216 public function hasClaim($name)
217 {
218 return isset($this->claims->{$name});
219 }
220
229 public function getClaim($name, $defaultValue = null)
230 {
231 if ($this->hasClaim($name)) {
232 $value = $this->claims->{$name};
233 } else {
234 $value = $defaultValue;
235 }
236
237 return $value;
238 }
239
245 public function getPayload()
246 {
247 return $this->claims;
248 }
249
255 public static function getLastPayload()
256 {
257 return self::$lastPayload;
258 }
259
268 public function verify($publicKey, $jku = null)
269 {
270 $ok = false;
271 $hasPublicKey = !empty($publicKey);
272 $retry = false;
273 $leeway = Jwt::$leeway;
274 do {
275 try {
276 $claimCheckerManager = new Checker\ClaimCheckerManager(
277 [
278 new Checker\IssuedAtChecker($leeway),
279 new Checker\NotBeforeChecker($leeway),
280 new Checker\ExpirationTimeChecker($leeway)
281 ]
282 );
283 $claimCheckerManager->check(Util::jsonDecode($this->jwt->getPayload(), true));
284 $algorithmManager = new Core\AlgorithmManager([
285 new Signature\Algorithm\RS256(),
286 new Signature\Algorithm\RS384(),
287 new Signature\Algorithm\RS512()
288 ]);
289 $jwsVerifier = new Signature\JWSVerifier(
290 $algorithmManager
291 );
292 switch ($this->getHeader('alg')) {
293 case 'RS256':
294 case 'RS384':
295 case 'RS512':
296 if ($this->hasHeader('kid') && ((Jwt::$allowJkuHeader && $this->hasHeader('jku')) || (!empty($jku) && empty($publicKey)))) {
297 if (Jwt::$allowJkuHeader && $this->hasHeader('jku')) {
298 $jwksUrl = $this->getHeader('jku');
299 } else {
300 $jwksUrl = $jku;
301 }
302 $jwks = $this->fetchPublicKey($jwksUrl, $this->getHeader('kid'));
303 $ok = $jwsVerifier->verifyWithKeySet($this->jwt, $jwks, 0);
304 } else {
305 $json = Util::jsonDecode($publicKey, true);
306 if (is_null($json)) {
307 $jwk = self::getJwk($publicKey, ['alg' => $this->getHeader('alg'), 'use' => 'sig']);
308 } else {
309 $jwk = new Core\JWK($json);
310 }
311 $ok = $jwsVerifier->verifyWithKey($this->jwt, $jwk, 0);
312 }
313 break;
314 }
315 } catch (\Exception $e) {
316 Util::logError($e->getMessage());
317 } catch (\TypeError $e) {
318 Util::logError($e->getMessage());
319 }
320 if (!$ok) {
321 if ($retry) {
322 $retry = false;
323 } elseif ($hasPublicKey && !empty($jku)) {
324 $retry = true;
325 $publicKey = null;
326 $hasPublicKey = false;
327 }
328 }
329 } while (!$ok && $retry);
330
331 return $ok;
332 }
333
348 public static function sign($payload, $signatureMethod, $privateKey, $kid = null, $jku = null, $encryptionMethod = null,
349 $publicKey = null)
350 {
351 switch ($signatureMethod) {
352 case 'RS512':
353 $sig = new Signature\Algorithm\RS512();
354 break;
355 case 'RS384':
356 $sig = new Signature\Algorithm\RS384();
357 break;
358 default:
359 $signatureMethod = 'RS256';
360 $sig = new Signature\Algorithm\RS256();
361 break;
362 }
363 $jwk = self::getJwk($privateKey, ['alg' => $signatureMethod, 'use' => 'sig']);
364 $headers = ['typ' => 'JWT', 'alg' => $signatureMethod];
365 if (!empty($kid)) {
366 $headers['kid'] = $kid;
367 if (!empty($jku)) {
368 $headers['jku'] = $jku;
369 }
370 }
371 $algorithmManager = new Core\AlgorithmManager(
372 [
373 new Signature\Algorithm\RS256(),
374 new Signature\Algorithm\RS384(),
375 new Signature\Algorithm\RS512()
376 ]
377 );
378 $jwsBuilder = new Signature\JWSBuilder($algorithmManager);
379 $jsonPayload = json_encode($payload);
380 $jws = $jwsBuilder->create()
381 ->withPayload($jsonPayload)
382 ->addSignature($jwk, $headers)
383 ->build();
384 $serializer = new Signature\Serializer\CompactSerializer();
385 $jwt = $serializer->serialize($jws);
386 if (!empty($encryptionMethod)) {
387 if (!empty($publicKey)) {
388 $keyEnc = 'RSA-OAEP-256';
389 $jwk = self::getJwk($publicKey, ['alg' => $keyEnc, 'use' => 'enc', 'zip' => 'DEF']);
390 $keyEncryptionAlgorithmManager = new Core\AlgorithmManager([new Encryption\Algorithm\KeyEncryption\RSAOAEP256()]);
391 $contentEncryptionAlgorithmManager = new Core\AlgorithmManager(
392 [
393 new Encryption\Algorithm\ContentEncryption\A128CBCHS256(),
394 new Encryption\Algorithm\ContentEncryption\A192CBCHS384(),
395 new Encryption\Algorithm\ContentEncryption\A256CBCHS512(),
396 ]
397 );
398 $compressionMethodManager = new Encryption\Compression\CompressionMethodManager([new Encryption\Compression\Deflate()]);
399 $jweBuilder = new Encryption\JWEBuilder($keyEncryptionAlgorithmManager, $contentEncryptionAlgorithmManager,
400 $compressionMethodManager);
401 $jwe = $jweBuilder
402 ->create()
403 ->withPayload($jwt)
404 ->withSharedProtectedHeader(['alg' => $keyEnc, 'enc' => $encryptionMethod, 'zip' => 'DEF'])
405 ->addRecipient($jwk)
406 ->build();
407 $serializer = new Encryption\Serializer\CompactSerializer();
408 $jwt = $serializer->serialize($jwe);
409 } else {
410 $errorMessage = 'No public key provided for encrypting JWT content';
411 Util::logError($errorMessage);
412 throw new \Exception($errorMessage);
413 }
414 }
415 self::$lastHeaders = $headers;
416 self::$lastPayload = $payload;
417
418 return $jwt;
419 }
420
428 public static function generateKey($signatureMethod = 'RS256')
429 {
430 return FirebaseClient::generateKey($signatureMethod);
431 }
432
440 public static function getPublicKey($privateKey)
441 {
442 return FirebaseClient::getPublicKey($privateKey);
443 }
444
454 public static function getJWKS($pemKey, $signatureMethod, $kid)
455 {
456 $keys['keys'] = array();
457 $additionalValues = ['alg' => $signatureMethod, 'use' => 'sig'];
458 if (!empty($kid)) {
459 $additionalValues['kid'] = $kid;
460 }
461 try {
462 $jwk = KeyManagement\JWKFactory::createFromKey($pemKey, null, $additionalValues);
463 $jwk = $jwk->toPublic();
464 $rsa = KeyManagement\KeyConverter\RSAKey::createFromJWK($jwk);
465 $rsa = $rsa::toPublic($rsa);
466 $keys['keys'][] = $rsa->toArray();
467 } catch (\Exception $e) {
468
469 }
470
471 return $keys;
472 }
473
474###
475### PRIVATE METHODS
476###
477
485 private function decrypt($privateKey)
486 {
487 $ok = false;
488 if ($this->jwt instanceof Encryption\JWE) {
489 $this->jwe = clone $this->jwt;
490 $keyEnc = $this->jwe->getSharedProtectedHeaderParameter('alg');
491 $jwk = KeyManagement\JWKFactory::createFromKey($privateKey, null, ['alg' => $keyEnc, 'use' => 'enc']);
492 $keyEncryptionAlgorithmManager = new Core\AlgorithmManager([new Encryption\Algorithm\KeyEncryption\RSAOAEP256()]);
493 $contentEncryptionAlgorithmManager = new Core\AlgorithmManager(
494 [
495 new Encryption\Algorithm\ContentEncryption\A128CBCHS256(),
496 new Encryption\Algorithm\ContentEncryption\A192CBCHS384(),
497 new Encryption\Algorithm\ContentEncryption\A256CBCHS512()
498 ]
499 );
500 $compressionMethodManager = new Encryption\Compression\CompressionMethodManager([new Encryption\Compression\Deflate()]);
501 $jweDecrypter = new Encryption\JWEDecrypter($keyEncryptionAlgorithmManager, $contentEncryptionAlgorithmManager,
502 $compressionMethodManager);
503 if ($jweDecrypter->decryptUsingKey($this->jwt, $jwk, 0)) {
504 try {
505 $jwt = $this->jwt->getPayload();
506 $serializer = new Signature\Serializer\CompactSerializer();
507 $this->jwt = $serializer->unserialize($jwt);
508 $ok = true;
509 } catch (\Exception $e) {
510 $ok = false;
511 }
512 }
513 }
514
515 return $ok;
516 }
517
526 private static function getJwk($key, $additionalValues)
527 {
528 $keyValues = Util::jsonDecode($key, true);
529 if (!is_array($keyValues)) {
530 $jwk = KeyManagement\JWKFactory::createFromKey($key, null, $additionalValues);
531 } else {
532 $keyValues = array_merge($keyValues, $additionalValues);
533 $jwk = new Core\JWK($keyValues);
534 }
535
536 return $jwk;
537 }
538
547 private function fetchPublicKey($jku, $kid)
548 {
549 $publicKey = null;
550 $http = new HttpMessage($jku);
551 if ($http->send()) {
552 $keys = Core\Util\JsonConverter::decode($http->response);
553 foreach ($keys['keys'] as $id => $key) {
554 if (!isset($key['kid']) || ($key['kid'] !== $kid)) {
555 unset($keys['keys'][$id]);
556 }
557 }
558 $publicKey = Core\JWKSet::createFromKeyData($keys);
559 }
560
561 return $publicKey;
562 }
563
564}
Class to represent an HTTP message request.
Class to represent an HTTP message request.
Definition Jwt.php:15
Class to implement the JWT interface using the Web Token JWT Framework library from https://web-token...
static getLastHeaders()
Get the value of the headers for the last signed JWT (before any encryption).
static getLastPayload()
Get the value of the payload for the last signed JWT (before any encryption).
getHeaders()
Get the value of the headers.
static getJWKS($pemKey, $signatureMethod, $kid)
Get the public JWKS from a key in PEM format.
hasJwt()
Check if a JWT is defined.
getJweHeaders()
Get the value of the JWE headers.
static getPublicKey($privateKey)
Get the public key for a private key.
load($jwtString, $privateKey=null)
Load a JWT from a string.
static generateKey($signatureMethod='RS256')
Generate a new private key in PEM format.
const SUPPORTED_ALGORITHMS
Supported signature algorithms.
isEncrypted()
Check if a JWT's content is encrypted.
static getSupportedAlgorithms()
Return an array of supported signature algorithms.
getHeader($name, $defaultValue=null)
Get the value of the header with the specified name.
getClaim($name, $defaultValue=null)
Get the value of the claim with the specified name.
static sign($payload, $signatureMethod, $privateKey, $kid=null, $jku=null, $encryptionMethod=null, $publicKey=null)
Sign the JWT.
hasHeader($name)
Check whether a JWT has a header with the specified name.
getPayload()
Get the value of the payload.
hasClaim($name)
Check whether a JWT has a claim with the specified name.
verify($publicKey, $jku=null)
Verify the signature of the JWT.
Class to implement utility methods.
Definition Util.php:15
static jsonDecode($str, $associative=false)
Decode a JSON string.
Definition Util.php:560
Interface to represent an HWT client.