source: vendor/google/auth/src/Credentials/ExternalAccountCredentials.php@ e3d4e0a

Last change on this file since e3d4e0a was e3d4e0a, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload project files

  • Property mode set to 100644
File size: 13.8 KB
Line 
1<?php
2/*
3 * Copyright 2023 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth\Credentials;
19
20use Google\Auth\CredentialSource\AwsNativeSource;
21use Google\Auth\CredentialSource\ExecutableSource;
22use Google\Auth\CredentialSource\FileSource;
23use Google\Auth\CredentialSource\UrlSource;
24use Google\Auth\ExecutableHandler\ExecutableHandler;
25use Google\Auth\ExternalAccountCredentialSourceInterface;
26use Google\Auth\FetchAuthTokenInterface;
27use Google\Auth\GetQuotaProjectInterface;
28use Google\Auth\GetUniverseDomainInterface;
29use Google\Auth\HttpHandler\HttpClientCache;
30use Google\Auth\HttpHandler\HttpHandlerFactory;
31use Google\Auth\OAuth2;
32use Google\Auth\ProjectIdProviderInterface;
33use Google\Auth\UpdateMetadataInterface;
34use Google\Auth\UpdateMetadataTrait;
35use GuzzleHttp\Psr7\Request;
36use InvalidArgumentException;
37
38class ExternalAccountCredentials implements
39 FetchAuthTokenInterface,
40 UpdateMetadataInterface,
41 GetQuotaProjectInterface,
42 GetUniverseDomainInterface,
43 ProjectIdProviderInterface
44{
45 use UpdateMetadataTrait;
46
47 private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
48 private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';
49
50 private OAuth2 $auth;
51 private ?string $quotaProject;
52 private ?string $serviceAccountImpersonationUrl;
53 private ?string $workforcePoolUserProject;
54 private ?string $projectId;
55 private string $universeDomain;
56
57 /**
58 * @param string|string[] $scope The scope of the access request, expressed either as an array
59 * or as a space-delimited string.
60 * @param array<mixed> $jsonKey JSON credentials as an associative array.
61 */
62 public function __construct(
63 $scope,
64 array $jsonKey
65 ) {
66 if (!array_key_exists('type', $jsonKey)) {
67 throw new InvalidArgumentException('json key is missing the type field');
68 }
69 if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
70 throw new InvalidArgumentException(sprintf(
71 'expected "%s" type but received "%s"',
72 self::EXTERNAL_ACCOUNT_TYPE,
73 $jsonKey['type']
74 ));
75 }
76
77 if (!array_key_exists('token_url', $jsonKey)) {
78 throw new InvalidArgumentException(
79 'json key is missing the token_url field'
80 );
81 }
82
83 if (!array_key_exists('audience', $jsonKey)) {
84 throw new InvalidArgumentException(
85 'json key is missing the audience field'
86 );
87 }
88
89 if (!array_key_exists('subject_token_type', $jsonKey)) {
90 throw new InvalidArgumentException(
91 'json key is missing the subject_token_type field'
92 );
93 }
94
95 if (!array_key_exists('credential_source', $jsonKey)) {
96 throw new InvalidArgumentException(
97 'json key is missing the credential_source field'
98 );
99 }
100
101 $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null;
102
103 $this->quotaProject = $jsonKey['quota_project_id'] ?? null;
104 $this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
105 $this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
106
107 $this->auth = new OAuth2([
108 'tokenCredentialUri' => $jsonKey['token_url'],
109 'audience' => $jsonKey['audience'],
110 'scope' => $scope,
111 'subjectTokenType' => $jsonKey['subject_token_type'],
112 'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
113 'additionalOptions' => $this->workforcePoolUserProject
114 ? ['userProject' => $this->workforcePoolUserProject]
115 : [],
116 ]);
117
118 if (!$this->isWorkforcePool() && $this->workforcePoolUserProject) {
119 throw new InvalidArgumentException(
120 'workforce_pool_user_project should not be set for non-workforce pool credentials.'
121 );
122 }
123 }
124
125 /**
126 * @param array<mixed> $jsonKey
127 */
128 private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
129 {
130 $credentialSource = $jsonKey['credential_source'];
131 if (isset($credentialSource['file'])) {
132 return new FileSource(
133 $credentialSource['file'],
134 $credentialSource['format']['type'] ?? null,
135 $credentialSource['format']['subject_token_field_name'] ?? null
136 );
137 }
138
139 if (
140 isset($credentialSource['environment_id'])
141 && 1 === preg_match('/^aws(\d+)$/', $credentialSource['environment_id'], $matches)
142 ) {
143 if ($matches[1] !== '1') {
144 throw new InvalidArgumentException(
145 "aws version \"$matches[1]\" is not supported in the current build."
146 );
147 }
148 if (!array_key_exists('regional_cred_verification_url', $credentialSource)) {
149 throw new InvalidArgumentException(
150 'The regional_cred_verification_url field is required for aws1 credential source.'
151 );
152 }
153
154 return new AwsNativeSource(
155 $jsonKey['audience'],
156 $credentialSource['regional_cred_verification_url'], // $regionalCredVerificationUrl
157 $credentialSource['region_url'] ?? null, // $regionUrl
158 $credentialSource['url'] ?? null, // $securityCredentialsUrl
159 $credentialSource['imdsv2_session_token_url'] ?? null, // $imdsV2TokenUrl
160 );
161 }
162
163 if (isset($credentialSource['url'])) {
164 return new UrlSource(
165 $credentialSource['url'],
166 $credentialSource['format']['type'] ?? null,
167 $credentialSource['format']['subject_token_field_name'] ?? null,
168 $credentialSource['headers'] ?? null,
169 );
170 }
171
172 if (isset($credentialSource['executable'])) {
173 if (!array_key_exists('command', $credentialSource['executable'])) {
174 throw new InvalidArgumentException(
175 'executable source requires a command to be set in the JSON file.'
176 );
177 }
178
179 // Build command environment variables
180 $env = [
181 'GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE' => $jsonKey['audience'],
182 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE' => $jsonKey['subject_token_type'],
183 // Always set to 0 because interactive mode is not supported.
184 'GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE' => '0',
185 ];
186
187 if ($outputFile = $credentialSource['executable']['output_file'] ?? null) {
188 $env['GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE'] = $outputFile;
189 }
190
191 if ($serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null) {
192 // Parse email from URL. The formal looks as follows:
193 // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
194 $regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
195 if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
196 $env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $matches['email'];
197 }
198 }
199
200 $timeoutMs = $credentialSource['executable']['timeout_millis'] ?? null;
201
202 return new ExecutableSource(
203 $credentialSource['executable']['command'],
204 $outputFile,
205 $timeoutMs ? new ExecutableHandler($env, $timeoutMs) : new ExecutableHandler($env)
206 );
207 }
208
209 throw new InvalidArgumentException('Unable to determine credential source from json key.');
210 }
211
212 /**
213 * @param string $stsToken
214 * @param callable|null $httpHandler
215 *
216 * @return array<mixed> {
217 * A set of auth related metadata, containing the following
218 *
219 * @type string $access_token
220 * @type int $expires_at
221 * }
222 */
223 private function getImpersonatedAccessToken(string $stsToken, ?callable $httpHandler = null): array
224 {
225 if (!isset($this->serviceAccountImpersonationUrl)) {
226 throw new InvalidArgumentException(
227 'service_account_impersonation_url must be set in JSON credentials.'
228 );
229 }
230 $request = new Request(
231 'POST',
232 $this->serviceAccountImpersonationUrl,
233 [
234 'Content-Type' => 'application/json',
235 'Authorization' => 'Bearer ' . $stsToken,
236 ],
237 (string) json_encode([
238 'lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS),
239 'scope' => explode(' ', $this->auth->getScope()),
240 ]),
241 );
242 if (is_null($httpHandler)) {
243 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
244 }
245 $response = $httpHandler($request);
246 $body = json_decode((string) $response->getBody(), true);
247 return [
248 'access_token' => $body['accessToken'],
249 'expires_at' => strtotime($body['expireTime']),
250 ];
251 }
252
253 /**
254 * @param callable|null $httpHandler
255 *
256 * @return array<mixed> {
257 * A set of auth related metadata, containing the following
258 *
259 * @type string $access_token
260 * @type int $expires_at (impersonated service accounts only)
261 * @type int $expires_in (identity pool only)
262 * @type string $issued_token_type (identity pool only)
263 * @type string $token_type (identity pool only)
264 * }
265 */
266 public function fetchAuthToken(?callable $httpHandler = null)
267 {
268 $stsToken = $this->auth->fetchAuthToken($httpHandler);
269
270 if (isset($this->serviceAccountImpersonationUrl)) {
271 return $this->getImpersonatedAccessToken($stsToken['access_token'], $httpHandler);
272 }
273
274 return $stsToken;
275 }
276
277 /**
278 * Get the cache token key for the credentials.
279 * The cache token key format depends on the type of source
280 * The format for the cache key one of the following:
281 * FetcherCacheKey.Scope.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
282 * FetcherCacheKey.Audience.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
283 *
284 * @return ?string;
285 */
286 public function getCacheKey(): ?string
287 {
288 $scopeOrAudience = $this->auth->getAudience();
289 if (!$scopeOrAudience) {
290 $scopeOrAudience = $this->auth->getScope();
291 }
292
293 return $this->auth->getSubjectTokenFetcher()->getCacheKey() .
294 '.' . $scopeOrAudience .
295 '.' . ($this->serviceAccountImpersonationUrl ?? '') .
296 '.' . ($this->auth->getSubjectTokenType() ?? '') .
297 '.' . ($this->workforcePoolUserProject ?? '');
298 }
299
300 public function getLastReceivedToken()
301 {
302 return $this->auth->getLastReceivedToken();
303 }
304
305 /**
306 * Get the quota project used for this API request
307 *
308 * @return string|null
309 */
310 public function getQuotaProject()
311 {
312 return $this->quotaProject;
313 }
314
315 /**
316 * Get the universe domain used for this API request
317 *
318 * @return string
319 */
320 public function getUniverseDomain(): string
321 {
322 return $this->universeDomain;
323 }
324
325 /**
326 * Get the project ID.
327 *
328 * @param callable|null $httpHandler Callback which delivers psr7 request
329 * @param string|null $accessToken The access token to use to sign the blob. If
330 * provided, saves a call to the metadata server for a new access
331 * token. **Defaults to** `null`.
332 * @return string|null
333 */
334 public function getProjectId(?callable $httpHandler = null, ?string $accessToken = null)
335 {
336 if (isset($this->projectId)) {
337 return $this->projectId;
338 }
339
340 $projectNumber = $this->getProjectNumber() ?: $this->workforcePoolUserProject;
341 if (!$projectNumber) {
342 return null;
343 }
344
345 if (is_null($httpHandler)) {
346 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
347 }
348
349 $url = str_replace(
350 'UNIVERSE_DOMAIN',
351 $this->getUniverseDomain(),
352 sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber)
353 );
354
355 if (is_null($accessToken)) {
356 $accessToken = $this->fetchAuthToken($httpHandler)['access_token'];
357 }
358
359 $request = new Request('GET', $url, ['authorization' => 'Bearer ' . $accessToken]);
360 $response = $httpHandler($request);
361
362 $body = json_decode((string) $response->getBody(), true);
363 return $this->projectId = $body['projectId'];
364 }
365
366 private function getProjectNumber(): ?string
367 {
368 $parts = explode('/', $this->auth->getAudience());
369 $i = array_search('projects', $parts);
370 return $parts[$i + 1] ?? null;
371 }
372
373 private function isWorkforcePool(): bool
374 {
375 $regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
376 return preg_match($regex, $this->auth->getAudience()) === 1;
377 }
378}
Note: See TracBrowser for help on using the repository browser.