|
2 | 2 |
|
3 | 3 | namespace Stackkit\LaravelGoogleCloudScheduler; |
4 | 4 |
|
5 | | -use Carbon\Carbon; |
6 | | -use Firebase\JWT\JWT; |
7 | | -use Firebase\JWT\SignatureInvalidException; |
8 | | -use GuzzleHttp\Client; |
9 | | -use GuzzleHttp\Exception\ServerException; |
10 | | -use Illuminate\Support\Arr; |
11 | | -use Illuminate\Support\Facades\Cache; |
12 | | -use phpseclib\Crypt\RSA; |
13 | | -use phpseclib\Math\BigInteger; |
14 | | -use Throwable; |
| 5 | +use Illuminate\Support\Facades\Facade; |
15 | 6 |
|
16 | | -class OpenIdVerificator |
| 7 | +class OpenIdVerificator extends Facade |
17 | 8 | { |
18 | | - private const V3_CERTS = 'GOOGLE_V3_CERTS'; |
19 | | - private const URL_OPENID_CONFIG = 'https://accounts.google.com/.well-known/openid-configuration'; |
20 | | - private const URL_TOKEN_INFO = 'https://www.googleapis.com/oauth2/v3/tokeninfo'; |
21 | | - |
22 | | - private $guzzle; |
23 | | - private $rsa; |
24 | | - private $jwt; |
25 | | - private $maxAge = []; |
26 | | - |
27 | | - public function __construct(Client $guzzle, RSA $rsa, JWT $jwt) |
28 | | - { |
29 | | - $this->guzzle = $guzzle; |
30 | | - $this->rsa = $rsa; |
31 | | - $this->jwt = $jwt; |
32 | | - } |
33 | | - |
34 | | - public function guardAgainstInvalidOpenIdToken($decodedToken) |
35 | | - { |
36 | | - /** |
37 | | - * https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken |
38 | | - */ |
39 | | - if (!in_array($decodedToken->iss, ['https://accounts.google.com', 'accounts.google.com'])) { |
40 | | - throw new CloudSchedulerException('The given OpenID token is not valid'); |
41 | | - } |
42 | | - |
43 | | - if ($decodedToken->exp < time()) { |
44 | | - throw new CloudSchedulerException('The given OpenID token has expired'); |
45 | | - } |
46 | | - |
47 | | - if ($decodedToken->aud !== config('laravel-google-cloud-scheduler.app_url')) { |
48 | | - throw new CloudSchedulerException('The given OpenID token is not valid'); |
49 | | - } |
50 | | - } |
51 | | - |
52 | | - public function decodeOpenIdToken($openIdToken, $kid, $cache = true) |
53 | | - { |
54 | | - if (!$cache) { |
55 | | - $this->forgetFromCache(); |
56 | | - } |
57 | | - |
58 | | - $publicKey = $this->getPublicKey($kid); |
59 | | - |
60 | | - try { |
61 | | - return $this->jwt->decode($openIdToken, $publicKey, ['RS256']); |
62 | | - } catch (SignatureInvalidException $e) { |
63 | | - if (!$cache) { |
64 | | - throw $e; |
65 | | - } |
66 | | - |
67 | | - return $this->decodeOpenIdToken($openIdToken, $kid, false); |
68 | | - } |
69 | | - } |
70 | | - |
71 | | - public function getPublicKey($kid = null) |
72 | | - { |
73 | | - if (Cache::has(self::V3_CERTS)) { |
74 | | - $v3Certs = Cache::get(self::V3_CERTS); |
75 | | - } else { |
76 | | - $v3Certs = $this->getFreshCertificates(); |
77 | | - Cache::put(self::V3_CERTS, $v3Certs, Carbon::now()->addSeconds($this->maxAge[self::URL_OPENID_CONFIG])); |
78 | | - } |
79 | | - |
80 | | - $cert = $kid ? collect($v3Certs)->firstWhere('kid', '=', $kid) : $v3Certs[0]; |
81 | | - |
82 | | - return $this->extractPublicKeyFromCertificate($cert); |
83 | | - } |
84 | | - |
85 | | - private function getFreshCertificates() |
86 | | - { |
87 | | - $jwksUri = $this->callApiAndReturnValue(self::URL_OPENID_CONFIG, 'jwks_uri'); |
88 | | - |
89 | | - return $this->callApiAndReturnValue($jwksUri, 'keys'); |
90 | | - } |
91 | | - |
92 | | - private function extractPublicKeyFromCertificate($certificate) |
93 | | - { |
94 | | - $modulus = new BigInteger(JWT::urlsafeB64Decode($certificate['n']), 256); |
95 | | - $exponent = new BigInteger(JWT::urlsafeB64Decode($certificate['e']), 256); |
96 | | - |
97 | | - $this->rsa->loadKey(compact('modulus', 'exponent')); |
98 | | - |
99 | | - return $this->rsa->getPublicKey(); |
100 | | - } |
101 | | - |
102 | | - public function getKidFromOpenIdToken($openIdToken) |
103 | | - { |
104 | | - return $this->callApiAndReturnValue(self::URL_TOKEN_INFO . '?id_token=' . $openIdToken, 'kid'); |
105 | | - } |
106 | | - |
107 | | - private function callApiAndReturnValue($url, $value) |
108 | | - { |
109 | | - $attempts = 0; |
110 | | - |
111 | | - while (true) { |
112 | | - try { |
113 | | - $response = $this->guzzle->get($url); |
114 | | - |
115 | | - break; |
116 | | - } catch (ServerException $e) { |
117 | | - $attempts++; |
118 | | - |
119 | | - if ($attempts >= 3) { |
120 | | - throw $e; |
121 | | - } |
122 | | - |
123 | | - sleep(1); |
124 | | - } |
125 | | - } |
126 | | - |
127 | | - $data = json_decode($response->getBody(), true); |
128 | | - |
129 | | - $maxAge = 0; |
130 | | - foreach ($response->getHeader('Cache-Control') as $line) { |
131 | | - preg_match('/max-age=(\d+)/', $line, $matches); |
132 | | - $maxAge = isset($matches[1]) ? (int) $matches[1] : 0; |
133 | | - } |
134 | | - |
135 | | - $this->maxAge[$url] = $maxAge; |
136 | | - |
137 | | - return Arr::get($data, $value); |
138 | | - } |
139 | | - |
140 | | - public function isCached() |
| 9 | + protected static function getFacadeAccessor() |
141 | 10 | { |
142 | | - return Cache::has(self::V3_CERTS); |
| 11 | + return 'open-id-verificator'; |
143 | 12 | } |
144 | 13 |
|
145 | | - public function forgetFromCache() |
| 14 | + public static function fake(): void |
146 | 15 | { |
147 | | - Cache::forget(self::V3_CERTS); |
| 16 | + self::swap(new OpenIdVerificatorFake()); |
148 | 17 | } |
149 | 18 | } |
0 commit comments