-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJwksCache.php
More file actions
92 lines (73 loc) · 2.88 KB
/
Copy pathJwksCache.php
File metadata and controls
92 lines (73 loc) · 2.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php
declare(strict_types=1);
namespace App\Services;
use App\Exceptions\JwksUnavailableException;
use App\Exceptions\WebhookVerificationException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class JwksCache
{
/**
* Resolve the JWK for $kid, refreshing the cache once if it's missing.
*
* A cached JWKS won't contain a key rotated in after it was fetched, so a
* miss on the first (cached) lookup doesn't necessarily mean the kid is
* bogus — it may just mean our cache predates the sender's rotation.
* Only report an unknown key (401, no retry) after a fresh fetch also
* fails to find it.
*/
public function resolveKey(string $url, string $kid): array
{
$jwk = $this->findKey($this->fetch($url), $kid);
if ($jwk !== null) {
return $jwk;
}
$jwk = $this->findKey($this->fetch($url, forceRefresh: true), $kid);
if ($jwk === null) {
throw new WebhookVerificationException("No JWKS key found with kid=\"{$kid}\"");
}
return $jwk;
}
private function fetch(string $url, bool $forceRefresh = false): array
{
$ttl = (int) config('webhook-receiver.jwks_cache_ttl', 3600);
$cacheKey = 'webhook_receiver.jwks.'.md5($url);
if ($forceRefresh) {
Cache::forget($cacheKey);
}
return Cache::remember($cacheKey, $ttl, function () use ($url) {
// Connection errors (timeouts, DNS, refused) already throw
// Illuminate\Http\Client\ConnectionException, which isn't caught
// by VerifyWebhookSignature and so becomes a 500 — matching how
// we want HTTP-level failures below to behave too.
$response = Http::timeout(5)->get($url);
if (! $response->successful()) {
throw new JwksUnavailableException(
"Failed to fetch JWKS from {$url}: HTTP {$response->status()}"
);
}
$data = $response->json();
if (! isset($data['keys']) || ! is_array($data['keys'])) {
throw new JwksUnavailableException('Invalid JWKS response: missing keys array');
}
return $data;
});
}
private function findKey(array $jwks, string $kid): ?array
{
foreach ($jwks['keys'] as $key) {
if (($key['kid'] ?? null) !== $kid) {
continue;
}
// A JWKS can publish encryption keys alongside signing keys.
// Dash's exported keys are "use": "sig", but only trust that
// explicitly rather than assuming an unmarked key is safe to
// verify with — skip anything declared for another purpose.
if (($key['use'] ?? 'sig') !== 'sig') {
continue;
}
return $key;
}
return null;
}
}