Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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,
1441 ariadna 83
        ?int $expiresAfter = null,
1 efrain 84
        bool $rateLimit = false,
1441 ariadna 85
        ?string $defaultAlg = null
1 efrain 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(
1441 ariadna 183
                    \sprintf('HTTP Error: %d %s for URI "%s"',
1 efrain 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);
1441 ariadna 215
 
216
        $cacheItemData = [];
217
        if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
218
            $cacheItemData = $data;
1 efrain 219
        }
220
 
1441 ariadna 221
        $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
222
        $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
223
 
1 efrain 224
        if (++$callsPerMinute > $this->maxCallsPerMinute) {
225
            return true;
226
        }
1441 ariadna 227
 
228
        $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
229
        $cacheItem->expiresAt($expiry);
1 efrain 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
}