Commit | Line | Data |
---|---|---|
9c976d32 TO |
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 | } |