Browse code

added appinfo/info.xml appinfo/signature.json CHANGELOG.txt lib/AppInfo/Application.php css/style.css providers/Plivo

DoubleBastionAdmin authored on 05/11/2025 13:35:09
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,274 @@
1
+<?php
2
+
3
+namespace Firebase\JWT;
4
+
5
+use ArrayAccess;
6
+use InvalidArgumentException;
7
+use LogicException;
8
+use OutOfBoundsException;
9
+use Psr\Cache\CacheItemInterface;
10
+use Psr\Cache\CacheItemPoolInterface;
11
+use Psr\Http\Client\ClientInterface;
12
+use Psr\Http\Message\RequestFactoryInterface;
13
+use RuntimeException;
14
+use UnexpectedValueException;
15
+
16
+/**
17
+ * @implements ArrayAccess<string, Key>
18
+ */
19
+class CachedKeySet implements ArrayAccess
20
+{
21
+    /**
22
+     * @var string
23
+     */
24
+    private $jwksUri;
25
+    /**
26
+     * @var ClientInterface
27
+     */
28
+    private $httpClient;
29
+    /**
30
+     * @var RequestFactoryInterface
31
+     */
32
+    private $httpFactory;
33
+    /**
34
+     * @var CacheItemPoolInterface
35
+     */
36
+    private $cache;
37
+    /**
38
+     * @var ?int
39
+     */
40
+    private $expiresAfter;
41
+    /**
42
+     * @var ?CacheItemInterface
43
+     */
44
+    private $cacheItem;
45
+    /**
46
+     * @var array<string, array<mixed>>
47
+     */
48
+    private $keySet;
49
+    /**
50
+     * @var string
51
+     */
52
+    private $cacheKey;
53
+    /**
54
+     * @var string
55
+     */
56
+    private $cacheKeyPrefix = 'jwks';
57
+    /**
58
+     * @var int
59
+     */
60
+    private $maxKeyLength = 64;
61
+    /**
62
+     * @var bool
63
+     */
64
+    private $rateLimit;
65
+    /**
66
+     * @var string
67
+     */
68
+    private $rateLimitCacheKey;
69
+    /**
70
+     * @var int
71
+     */
72
+    private $maxCallsPerMinute = 10;
73
+    /**
74
+     * @var string|null
75
+     */
76
+    private $defaultAlg;
77
+
78
+    public function __construct(
79
+        string $jwksUri,
80
+        ClientInterface $httpClient,
81
+        RequestFactoryInterface $httpFactory,
82
+        CacheItemPoolInterface $cache,
83
+        ?int $expiresAfter = null,
84
+        bool $rateLimit = false,
85
+        ?string $defaultAlg = null
86
+    ) {
87
+        $this->jwksUri = $jwksUri;
88
+        $this->httpClient = $httpClient;
89
+        $this->httpFactory = $httpFactory;
90
+        $this->cache = $cache;
91
+        $this->expiresAfter = $expiresAfter;
92
+        $this->rateLimit = $rateLimit;
93
+        $this->defaultAlg = $defaultAlg;
94
+        $this->setCacheKeys();
95
+    }
96
+
97
+    /**
98
+     * @param string $keyId
99
+     * @return Key
100
+     */
101
+    public function offsetGet($keyId): Key
102
+    {
103
+        if (!$this->keyIdExists($keyId)) {
104
+            throw new OutOfBoundsException('Key ID not found');
105
+        }
106
+        return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
107
+    }
108
+
109
+    /**
110
+     * @param string $keyId
111
+     * @return bool
112
+     */
113
+    public function offsetExists($keyId): bool
114
+    {
115
+        return $this->keyIdExists($keyId);
116
+    }
117
+
118
+    /**
119
+     * @param string $offset
120
+     * @param Key $value
121
+     */
122
+    public function offsetSet($offset, $value): void
123
+    {
124
+        throw new LogicException('Method not implemented');
125
+    }
126
+
127
+    /**
128
+     * @param string $offset
129
+     */
130
+    public function offsetUnset($offset): void
131
+    {
132
+        throw new LogicException('Method not implemented');
133
+    }
134
+
135
+    /**
136
+     * @return array<mixed>
137
+     */
138
+    private function formatJwksForCache(string $jwks): array
139
+    {
140
+        $jwks = json_decode($jwks, true);
141
+
142
+        if (!isset($jwks['keys'])) {
143
+            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
144
+        }
145
+
146
+        if (empty($jwks['keys'])) {
147
+            throw new InvalidArgumentException('JWK Set did not contain any keys');
148
+        }
149
+
150
+        $keys = [];
151
+        foreach ($jwks['keys'] as $k => $v) {
152
+            $kid = isset($v['kid']) ? $v['kid'] : $k;
153
+            $keys[(string) $kid] = $v;
154
+        }
155
+
156
+        return $keys;
157
+    }
158
+
159
+    private function keyIdExists(string $keyId): bool
160
+    {
161
+        if (null === $this->keySet) {
162
+            $item = $this->getCacheItem();
163
+            // Try to load keys from cache
164
+            if ($item->isHit()) {
165
+                // item found! retrieve it
166
+                $this->keySet = $item->get();
167
+                // If the cached item is a string, the JWKS response was cached (previous behavior).
168
+                // Parse this into expected format array<kid, jwk> instead.
169
+                if (\is_string($this->keySet)) {
170
+                    $this->keySet = $this->formatJwksForCache($this->keySet);
171
+                }
172
+            }
173
+        }
174
+
175
+        if (!isset($this->keySet[$keyId])) {
176
+            if ($this->rateLimitExceeded()) {
177
+                return false;
178
+            }
179
+            $request = $this->httpFactory->createRequest('GET', $this->jwksUri);
180
+            $jwksResponse = $this->httpClient->sendRequest($request);
181
+            if ($jwksResponse->getStatusCode() !== 200) {
182
+                throw new UnexpectedValueException(
183
+                    \sprintf('HTTP Error: %d %s for URI "%s"',
184
+                        $jwksResponse->getStatusCode(),
185
+                        $jwksResponse->getReasonPhrase(),
186
+                        $this->jwksUri,
187
+                    ),
188
+                    $jwksResponse->getStatusCode()
189
+                );
190
+            }
191
+            $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
192
+
193
+            if (!isset($this->keySet[$keyId])) {
194
+                return false;
195
+            }
196
+
197
+            $item = $this->getCacheItem();
198
+            $item->set($this->keySet);
199
+            if ($this->expiresAfter) {
200
+                $item->expiresAfter($this->expiresAfter);
201
+            }
202
+            $this->cache->save($item);
203
+        }
204
+
205
+        return true;
206
+    }
207
+
208
+    private function rateLimitExceeded(): bool
209
+    {
210
+        if (!$this->rateLimit) {
211
+            return false;
212
+        }
213
+
214
+        $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
215
+
216
+        $cacheItemData = [];
217
+        if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
218
+            $cacheItemData = $data;
219
+        }
220
+
221
+        $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
222
+        $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
223
+
224
+        if (++$callsPerMinute > $this->maxCallsPerMinute) {
225
+            return true;
226
+        }
227
+
228
+        $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
229
+        $cacheItem->expiresAt($expiry);
230
+        $this->cache->save($cacheItem);
231
+        return false;
232
+    }
233
+
234
+    private function getCacheItem(): CacheItemInterface
235
+    {
236
+        if (\is_null($this->cacheItem)) {
237
+            $this->cacheItem = $this->cache->getItem($this->cacheKey);
238
+        }
239
+
240
+        return $this->cacheItem;
241
+    }
242
+
243
+    private function setCacheKeys(): void
244
+    {
245
+        if (empty($this->jwksUri)) {
246
+            throw new RuntimeException('JWKS URI is empty');
247
+        }
248
+
249
+        // ensure we do not have illegal characters
250
+        $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
251
+
252
+        // add prefix
253
+        $key = $this->cacheKeyPrefix . $key;
254
+
255
+        // Hash keys if they exceed $maxKeyLength of 64
256
+        if (\strlen($key) > $this->maxKeyLength) {
257
+            $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
258
+        }
259
+
260
+        $this->cacheKey = $key;
261
+
262
+        if ($this->rateLimit) {
263
+            // add prefix
264
+            $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
265
+
266
+            // Hash keys if they exceed $maxKeyLength of 64
267
+            if (\strlen($rateLimitKey) > $this->maxKeyLength) {
268
+                $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
269
+            }
270
+
271
+            $this->rateLimitCacheKey = $rateLimitKey;
272
+        }
273
+    }
274
+}