Skip to content

Commit 50e438f

Browse files
authored
fix: PHP-JWT versioned to maintain support for composer and WP Bedrock (#778)
* fix: PHP-JWT versioned to maintain support for composer and WP Bedrock installations * devops: PHPStan compliance met
1 parent e684a51 commit 50e438f

File tree

9 files changed

+1407
-4
lines changed

9 files changed

+1407
-4
lines changed

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ node_modules/
1515
.vscode/settings.json
1616
.github_changelog_generator
1717
vendor/*
18-
!vendor-prefixed/
19-
vendor-prefixed/*
20-
!vendor-prefixed/.gitkeep
2118
!tests
2219
tests/*.suite.yml
2320
build/

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ parameters:
1515
- access-functions.php
1616
- includes/
1717
scanDirectories:
18-
- vendor/woographql/
18+
- vendor-prefixed/
1919
- local/public/wp-content/plugins/wp-graphql-jwt-authentication/
2020
- local/public/wp-content/plugins/woocommerce/src/Internal/DataStores/Orders/
2121
scanFiles:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
/**
3+
* @license BSD-3-Clause
4+
*
5+
* Modified by Geoff Taylor using Strauss.
6+
* @see https://github.com/BrianHenryIE/strauss
7+
*/
8+
9+
namespace WPGraphQL\WooCommerce\Vendor\Firebase\JWT;
10+
11+
class BeforeValidException extends \UnexpectedValueException
12+
{
13+
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
/**
3+
* @license BSD-3-Clause
4+
*
5+
* Modified by Geoff Taylor using Strauss.
6+
* @see https://github.com/BrianHenryIE/strauss
7+
*/
8+
9+
namespace WPGraphQL\WooCommerce\Vendor\Firebase\JWT;
10+
11+
use ArrayAccess;
12+
use InvalidArgumentException;
13+
use LogicException;
14+
use OutOfBoundsException;
15+
use Psr\Cache\CacheItemInterface;
16+
use Psr\Cache\CacheItemPoolInterface;
17+
use Psr\Http\Client\ClientInterface;
18+
use Psr\Http\Message\RequestFactoryInterface;
19+
use RuntimeException;
20+
use UnexpectedValueException;
21+
22+
/**
23+
* @implements ArrayAccess<string, Key>
24+
*/
25+
class CachedKeySet implements ArrayAccess
26+
{
27+
/**
28+
* @var string
29+
*/
30+
private $jwksUri;
31+
/**
32+
* @var ClientInterface
33+
*/
34+
private $httpClient;
35+
/**
36+
* @var RequestFactoryInterface
37+
*/
38+
private $httpFactory;
39+
/**
40+
* @var CacheItemPoolInterface
41+
*/
42+
private $cache;
43+
/**
44+
* @var ?int
45+
*/
46+
private $expiresAfter;
47+
/**
48+
* @var ?CacheItemInterface
49+
*/
50+
private $cacheItem;
51+
/**
52+
* @var array<string, array<mixed>>
53+
*/
54+
private $keySet;
55+
/**
56+
* @var string
57+
*/
58+
private $cacheKey;
59+
/**
60+
* @var string
61+
*/
62+
private $cacheKeyPrefix = 'jwks';
63+
/**
64+
* @var int
65+
*/
66+
private $maxKeyLength = 64;
67+
/**
68+
* @var bool
69+
*/
70+
private $rateLimit;
71+
/**
72+
* @var string
73+
*/
74+
private $rateLimitCacheKey;
75+
/**
76+
* @var int
77+
*/
78+
private $maxCallsPerMinute = 10;
79+
/**
80+
* @var string|null
81+
*/
82+
private $defaultAlg;
83+
84+
public function __construct(
85+
string $jwksUri,
86+
ClientInterface $httpClient,
87+
RequestFactoryInterface $httpFactory,
88+
CacheItemPoolInterface $cache,
89+
int $expiresAfter = null,
90+
bool $rateLimit = false,
91+
string $defaultAlg = null
92+
) {
93+
$this->jwksUri = $jwksUri;
94+
$this->httpClient = $httpClient;
95+
$this->httpFactory = $httpFactory;
96+
$this->cache = $cache;
97+
$this->expiresAfter = $expiresAfter;
98+
$this->rateLimit = $rateLimit;
99+
$this->defaultAlg = $defaultAlg;
100+
$this->setCacheKeys();
101+
}
102+
103+
/**
104+
* @param string $keyId
105+
* @return Key
106+
*/
107+
public function offsetGet($keyId): Key
108+
{
109+
if (!$this->keyIdExists($keyId)) {
110+
throw new OutOfBoundsException('Key ID not found');
111+
}
112+
return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
113+
}
114+
115+
/**
116+
* @param string $keyId
117+
* @return bool
118+
*/
119+
public function offsetExists($keyId): bool
120+
{
121+
return $this->keyIdExists($keyId);
122+
}
123+
124+
/**
125+
* @param string $offset
126+
* @param Key $value
127+
*/
128+
public function offsetSet($offset, $value): void
129+
{
130+
throw new LogicException('Method not implemented');
131+
}
132+
133+
/**
134+
* @param string $offset
135+
*/
136+
public function offsetUnset($offset): void
137+
{
138+
throw new LogicException('Method not implemented');
139+
}
140+
141+
/**
142+
* @return array<mixed>
143+
*/
144+
private function formatJwksForCache(string $jwks): array
145+
{
146+
$jwks = json_decode($jwks, true);
147+
148+
if (!isset($jwks['keys'])) {
149+
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
150+
}
151+
152+
if (empty($jwks['keys'])) {
153+
throw new InvalidArgumentException('JWK Set did not contain any keys');
154+
}
155+
156+
$keys = [];
157+
foreach ($jwks['keys'] as $k => $v) {
158+
$kid = isset($v['kid']) ? $v['kid'] : $k;
159+
$keys[(string) $kid] = $v;
160+
}
161+
162+
return $keys;
163+
}
164+
165+
private function keyIdExists(string $keyId): bool
166+
{
167+
if (null === $this->keySet) {
168+
$item = $this->getCacheItem();
169+
// Try to load keys from cache
170+
if ($item->isHit()) {
171+
// item found! retrieve it
172+
$this->keySet = $item->get();
173+
// If the cached item is a string, the JWKS response was cached (previous behavior).
174+
// Parse this into expected format array<kid, jwk> instead.
175+
if (\is_string($this->keySet)) {
176+
$this->keySet = $this->formatJwksForCache($this->keySet);
177+
}
178+
}
179+
}
180+
181+
if (!isset($this->keySet[$keyId])) {
182+
if ($this->rateLimitExceeded()) {
183+
return false;
184+
}
185+
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
186+
$jwksResponse = $this->httpClient->sendRequest($request);
187+
if ($jwksResponse->getStatusCode() !== 200) {
188+
throw new UnexpectedValueException(
189+
sprintf('HTTP Error: %d %s for URI "%s"',
190+
$jwksResponse->getStatusCode(),
191+
$jwksResponse->getReasonPhrase(),
192+
$this->jwksUri,
193+
),
194+
$jwksResponse->getStatusCode()
195+
);
196+
}
197+
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
198+
199+
if (!isset($this->keySet[$keyId])) {
200+
return false;
201+
}
202+
203+
$item = $this->getCacheItem();
204+
$item->set($this->keySet);
205+
if ($this->expiresAfter) {
206+
$item->expiresAfter($this->expiresAfter);
207+
}
208+
$this->cache->save($item);
209+
}
210+
211+
return true;
212+
}
213+
214+
private function rateLimitExceeded(): bool
215+
{
216+
if (!$this->rateLimit) {
217+
return false;
218+
}
219+
220+
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
221+
if (!$cacheItem->isHit()) {
222+
$cacheItem->expiresAfter(1); // # of calls are cached each minute
223+
}
224+
225+
$callsPerMinute = (int) $cacheItem->get();
226+
if (++$callsPerMinute > $this->maxCallsPerMinute) {
227+
return true;
228+
}
229+
$cacheItem->set($callsPerMinute);
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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
/**
3+
* @license BSD-3-Clause
4+
*
5+
* Modified by Geoff Taylor using Strauss.
6+
* @see https://github.com/BrianHenryIE/strauss
7+
*/
8+
9+
namespace WPGraphQL\WooCommerce\Vendor\Firebase\JWT;
10+
11+
class ExpiredException extends \UnexpectedValueException
12+
{
13+
}

0 commit comments

Comments
 (0)