Proyectos de Subversion Moodle

Rev

| 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,
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
        if (!$cacheItem->isHit()) {
216
            $cacheItem->expiresAfter(1); // # of calls are cached each minute
217
        }
218
 
219
        $callsPerMinute = (int) $cacheItem->get();
220
        if (++$callsPerMinute > $this->maxCallsPerMinute) {
221
            return true;
222
        }
223
        $cacheItem->set($callsPerMinute);
224
        $this->cache->save($cacheItem);
225
        return false;
226
    }
227
 
228
    private function getCacheItem(): CacheItemInterface
229
    {
230
        if (\is_null($this->cacheItem)) {
231
            $this->cacheItem = $this->cache->getItem($this->cacheKey);
232
        }
233
 
234
        return $this->cacheItem;
235
    }
236
 
237
    private function setCacheKeys(): void
238
    {
239
        if (empty($this->jwksUri)) {
240
            throw new RuntimeException('JWKS URI is empty');
241
        }
242
 
243
        // ensure we do not have illegal characters
244
        $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
245
 
246
        // add prefix
247
        $key = $this->cacheKeyPrefix . $key;
248
 
249
        // Hash keys if they exceed $maxKeyLength of 64
250
        if (\strlen($key) > $this->maxKeyLength) {
251
            $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
252
        }
253
 
254
        $this->cacheKey = $key;
255
 
256
        if ($this->rateLimit) {
257
            // add prefix
258
            $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
259
 
260
            // Hash keys if they exceed $maxKeyLength of 64
261
            if (\strlen($rateLimitKey) > $this->maxKeyLength) {
262
                $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
263
            }
264
 
265
            $this->rateLimitCacheKey = $rateLimitKey;
266
        }
267
    }
268
}