Merge pull request #23215 from eileenmcnaughton/test_amount
[civicrm-core.git] / Civi / Crypto / CryptoJwt.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Crypto;
13
14 use Civi\Crypto\Exception\CryptoException;
15 use Firebase\JWT\JWT;
16
17 /**
18 * The "Crypto JWT" service supports a token format suitable for
19 * exchanging/transmitting with external consumers (e.g. web-browsers).
20 * It integrates with the CryptoRegistry (which is a source of valid signing keys).
21 *
22 * By default, tokens are signed and validated using any 'SIGN'ing keys
23 * (ie 'CIVICRM_SIGN_KEYS').
24 *
25 * @package Civi\Crypto
26 * @see https://jwt.io/
27 */
28 class CryptoJwt {
29
30 /**
31 * @var \Civi\Crypto\CryptoRegistry
32 */
33 protected $registry;
34
35 /**
36 * @param array $payload
37 * List of JWT claims. See IANA link below.
38 * @param string $keyIdOrTag
39 * Choose a valid key from the CryptoRegistry using $keyIdOrTag.
40 * @return string
41 * @throws \Civi\Crypto\Exception\CryptoException
42 *
43 * @see https://www.iana.org/assignments/jwt/jwt.xhtml
44 */
45 public function encode($payload, $keyIdOrTag = 'SIGN') {
46 $key = $this->getRegistry()->findKey($keyIdOrTag);
47 $alg = $this->suiteToAlg($key['suite']);
48 // Currently, registry only has symmetric keys in $key['key']. For public key-pairs, might need to change.
49 return JWT::encode($payload, $key['key'], $alg, $key['id']);
50 }
51
52 /**
53 * @param string $token
54 * The JWT token.
55 * @param string $keyTag
56 * Lookup valid keys from the CryptoRegistry using $keyTag.
57 * @return array
58 * List of validated JWT claims.
59 * @throws CryptoException
60 */
61 public function decode($token, $keyTag = 'SIGN') {
62 $keyRows = $this->getRegistry()->findKeysByTag($keyTag);
63
64 // We want to call JWT::decode(), but there's a slight mismatch -- the
65 // registry contains whitelisted permutations of ($key,$alg), but
66 // JWT::decode() accepts all permutations ($keys x $algs).
67
68 // Grouping by alg will give proper granularity and also produces one
69 // call to JWT::decode() in typical usage.
70
71 // Defn: $keysByAlg[$alg][$keyId] === $keyData
72 $keysByAlg = [];
73 foreach ($keyRows as $key) {
74 if ($alg = $this->suiteToAlg($key['suite'])) {
75 // Currently, registry only has symmetric keys in $key['key']. For public key-pairs, might need to change.
76 $keysByAlg[$alg][$key['id']] = $key['key'];
77 }
78 }
79
80 foreach ($keysByAlg as $alg => $keys) {
81 try {
82 return (array) JWT::decode($token, $keys, [$alg]);
83 }
84 catch (\UnexpectedValueException $e) {
85 // Depending on the error, we might able to try other algos
86 if (
87 !preg_match(';unable to lookup correct key;', $e->getMessage())
88 &&
89 !preg_match(';Signature verification failed;', $e->getMessage())
90 ) {
91 // Keep our signature independent of the implementation.
92 throw new CryptoException(get_class($e) . ': ' . $e->getMessage());
93 }
94 }
95 }
96
97 throw new CryptoException('Signature verification failed');
98 }
99
100 /**
101 * @param string $suite
102 * Ex: 'jwt-hs256', 'jwt-hs384'
103 * @return string
104 * Ex: 'HS256', 'HS384'
105 */
106 protected static function suiteToAlg($suite) {
107 if (substr($suite, 0, 4) === 'jwt-') {
108 return strtoupper(substr($suite, 4));
109 }
110 else {
111 return NULL;
112 }
113 }
114
115 /**
116 * @return CryptoRegistry
117 */
118 protected function getRegistry(): CryptoRegistry {
119 if ($this->registry === NULL) {
120 $this->registry = \Civi::service('crypto.registry');
121 }
122 return $this->registry;
123 }
124
125 }