PluginProbe
Authorizer / 3.15.3
Authorizer v3.15.3
3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 2.9.6 All 126 releases
authorizer / vendor / google / auth / src / CredentialSource / AwsNativeSource.php

AwsNativeSource.php in Authorizer 3.15.3, at vendor/google/auth/src/CredentialSource/AwsNativeSource.php

437 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
18 namespace Google\Auth\CredentialSource;
19
20 use Google\Auth\ExternalAccountCredentialSourceInterface;
21 use Google\Auth\HttpHandler\HttpClientCache;
22 use Google\Auth\HttpHandler\HttpHandlerFactory;
23 use GuzzleHttp\Psr7\Request;
24
25 /**
26 * Authenticates requests using AWS credentials.
27 */
28 class AwsNativeSource implements ExternalAccountCredentialSourceInterface
29 {
30 private const CRED_VERIFICATION_QUERY = 'Action=GetCallerIdentity&Version=2011-06-15';
31 private const ECS_CONTAINER_METADATA_URL = 'http://169.254.170.2';
32
33 private string $audience;
34 private string $regionalCredVerificationUrl;
35 private ?string $regionUrl;
36 private ?string $securityCredentialsUrl;
37 private ?string $imdsv2SessionTokenUrl;
38
39 /**
40 * @param string $audience The audience for the credential.
41 * @param string $regionalCredVerificationUrl The regional AWS GetCallerIdentity action URL used to determine the
42 * AWS account ID and its roles. This is not called by this library, but
43 * is sent in the subject token to be called by the STS token server.
44 * @param string|null $regionUrl This URL should be used to determine the current AWS region needed for the signed
45 * request construction.
46 * @param string|null $securityCredentialsUrl The AWS metadata server URL used to retrieve the access key, secret
47 * key and security token needed to sign the GetCallerIdentity request.
48 * @param string|null $imdsv2SessionTokenUrl Presence of this URL enforces the auth libraries to fetch a Session
49 * Token from AWS. This field is required for EC2 instances using IMDSv2.
50 */
51 public function __construct(
52 string $audience,
53 string $regionalCredVerificationUrl,
54 ?string $regionUrl = null,
55 ?string $securityCredentialsUrl = null,
56 ?string $imdsv2SessionTokenUrl = null
57 ) {
58 $this->audience = $audience;
59 $this->regionalCredVerificationUrl = $regionalCredVerificationUrl;
60 $this->regionUrl = $regionUrl;
61 $this->securityCredentialsUrl = $securityCredentialsUrl;
62 $this->imdsv2SessionTokenUrl = $imdsv2SessionTokenUrl;
63 }
64
65 public function fetchSubjectToken(?callable $httpHandler = null): string
66 {
67 if (is_null($httpHandler)) {
68 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
69 }
70
71 $headers = [];
72 if ($this->imdsv2SessionTokenUrl) {
73 $headers = [
74 'X-aws-ec2-metadata-token' => self::getImdsV2SessionToken($this->imdsv2SessionTokenUrl, $httpHandler)
75 ];
76 }
77
78 $signingVars = self::getSigningVarsFromEnv()
79 ?? self::getSigningVarsFromEcs($httpHandler);
80
81 if (!$signingVars) {
82 if (!$this->securityCredentialsUrl) {
83 throw new \LogicException('Unable to get credentials from ENV, and no security credentials URL provided');
84 }
85 $signingVars = self::getSigningVarsFromUrl(
86 $httpHandler,
87 $this->securityCredentialsUrl,
88 self::getRoleName($httpHandler, $this->securityCredentialsUrl, $headers),
89 $headers
90 );
91 }
92
93 if (!$region = self::getRegionFromEnv()) {
94 if (!$this->regionUrl) {
95 throw new \LogicException('Unable to get region from ENV, and no region URL provided');
96 }
97 $region = self::getRegionFromUrl($httpHandler, $this->regionUrl, $headers);
98 }
99 $url = str_replace('{region}', $region, $this->regionalCredVerificationUrl);
100 $host = parse_url($url)['host'] ?? '';
101
102 // From here we use the signing vars to create the signed request to receive a token
103 [$accessKeyId, $secretAccessKey, $securityToken] = $signingVars;
104 $headers = self::getSignedRequestHeaders($region, $host, $accessKeyId, $secretAccessKey, $securityToken);
105
106 // Inject x-goog-cloud-target-resource into header
107 $headers['x-goog-cloud-target-resource'] = $this->audience;
108
109 // Format headers as they're expected in the subject token
110 $formattedHeaders = array_map(
111 fn ($k, $v) => ['key' => $k, 'value' => $v],
112 array_keys($headers),
113 $headers,
114 );
115
116 $request = [
117 'headers' => $formattedHeaders,
118 'method' => 'POST',
119 'url' => $url,
120 ];
121
122 return urlencode(json_encode($request) ?: '');
123 }
124
125 /**
126 * @internal
127 */
128 public static function getImdsV2SessionToken(string $imdsV2Url, callable $httpHandler): string
129 {
130 $headers = [
131 'X-aws-ec2-metadata-token-ttl-seconds' => '21600'
132 ];
133 $request = new Request(
134 'PUT',
135 $imdsV2Url,
136 $headers
137 );
138
139 $response = $httpHandler($request);
140 return (string) $response->getBody();
141 }
142
143 /**
144 * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
145 *
146 * @internal
147 *
148 * @return array<string, string>
149 */
150 public static function getSignedRequestHeaders(
151 string $region,
152 string $host,
153 string $accessKeyId,
154 string $secretAccessKey,
155 ?string $securityToken
156 ): array {
157 $service = 'sts';
158
159 # Create a date for headers and the credential string in ISO-8601 format
160 $amzdate = gmdate('Ymd\THis\Z');
161 $datestamp = gmdate('Ymd'); # Date w/o time, used in credential scope
162
163 # Create the canonical headers and signed headers. Header names
164 # must be trimmed and lowercase, and sorted in code point order from
165 # low to high. Note that there is a trailing \n.
166 $canonicalHeaders = sprintf("host:%s\nx-amz-date:%s\n", $host, $amzdate);
167 if ($securityToken) {
168 $canonicalHeaders .= sprintf("x-amz-security-token:%s\n", $securityToken);
169 }
170
171 # Step 5: Create the list of signed headers. This lists the headers
172 # in the canonicalHeaders list, delimited with ";" and in alpha order.
173 # Note: The request can include any headers; $canonicalHeaders and
174 # $signedHeaders lists those that you want to be included in the
175 # hash of the request. "Host" and "x-amz-date" are always required.
176 $signedHeaders = 'host;x-amz-date';
177 if ($securityToken) {
178 $signedHeaders .= ';x-amz-security-token';
179 }
180
181 # Step 6: Create payload hash (hash of the request body content). For GET
182 # requests, the payload is an empty string ("").
183 $payloadHash = hash('sha256', '');
184
185 # Step 7: Combine elements to create canonical request
186 $canonicalRequest = implode("\n", [
187 'POST', // method
188 '/', // canonical URL
189 self::CRED_VERIFICATION_QUERY, // query string
190 $canonicalHeaders,
191 $signedHeaders,
192 $payloadHash
193 ]);
194
195 # ************* TASK 2: CREATE THE STRING TO SIGN*************
196 # Match the algorithm to the hashing algorithm you use, either SHA-1 or
197 # SHA-256 (recommended)
198 $algorithm = 'AWS4-HMAC-SHA256';
199 $scope = implode('/', [$datestamp, $region, $service, 'aws4_request']);
200 $stringToSign = implode("\n", [$algorithm, $amzdate, $scope, hash('sha256', $canonicalRequest)]);
201
202 # ************* TASK 3: CALCULATE THE SIGNATURE *************
203 # Create the signing key using the function defined above.
204 // (done above)
205 $signingKey = self::getSignatureKey($secretAccessKey, $datestamp, $region, $service);
206
207 # Sign the string_to_sign using the signing_key
208 $signature = bin2hex(self::hmacSign($signingKey, $stringToSign));
209
210 # ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
211 # The signing information can be either in a query string value or in
212 # a header named Authorization. This code shows how to use a header.
213 # Create authorization header and add to request headers
214 $authorizationHeader = sprintf(
215 '%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
216 $algorithm,
217 $accessKeyId,
218 $scope,
219 $signedHeaders,
220 $signature
221 );
222
223 # The request can include any headers, but MUST include "host", "x-amz-date",
224 # and (for this scenario) "Authorization". "host" and "x-amz-date" must
225 # be included in the canonical_headers and signed_headers, as noted
226 # earlier. Order here is not significant.
227 $headers = [
228 'host' => $host,
229 'x-amz-date' => $amzdate,
230 'Authorization' => $authorizationHeader,
231 ];
232 if ($securityToken) {
233 $headers['x-amz-security-token'] = $securityToken;
234 }
235
236 return $headers;
237 }
238
239 /**
240 * @internal
241 */
242 public static function getRegionFromEnv(): ?string
243 {
244 $region = getenv('AWS_REGION');
245 if (empty($region)) {
246 $region = getenv('AWS_DEFAULT_REGION');
247 }
248 return $region ?: null;
249 }
250
251 /**
252 * @internal
253 *
254 * @param callable $httpHandler
255 * @param string $regionUrl
256 * @param array<string, string|string[]> $headers Request headers to send in with the request.
257 */
258 public static function getRegionFromUrl(callable $httpHandler, string $regionUrl, array $headers): string
259 {
260 // get the region/zone from the region URL
261 $regionRequest = new Request('GET', $regionUrl, $headers);
262 $regionResponse = $httpHandler($regionRequest);
263
264 // Remove last character. For example, if us-east-2b is returned,
265 // the region would be us-east-2.
266 return substr((string) $regionResponse->getBody(), 0, -1);
267 }
268
269 /**
270 * @internal
271 *
272 * @param callable $httpHandler
273 * @param string $securityCredentialsUrl
274 * @param array<string, string|string[]> $headers Request headers to send in with the request.
275 */
276 public static function getRoleName(callable $httpHandler, string $securityCredentialsUrl, array $headers): string
277 {
278 // Get the AWS role name
279 $roleRequest = new Request('GET', $securityCredentialsUrl, $headers);
280 $roleResponse = $httpHandler($roleRequest);
281 $roleName = (string) $roleResponse->getBody();
282
283 return $roleName;
284 }
285
286 /**
287 * @internal
288 *
289 * @param callable $httpHandler
290 * @param string $securityCredentialsUrl
291 * @param array<string, string|string[]> $headers Request headers to send in with the request.
292 * @return array{string, string, ?string}
293 */
294 public static function getSigningVarsFromUrl(
295 callable $httpHandler,
296 string $securityCredentialsUrl,
297 string $roleName,
298 array $headers
299 ): array {
300 // Get the AWS credentials
301 $credsRequest = new Request(
302 'GET',
303 $securityCredentialsUrl . '/' . $roleName,
304 $headers
305 );
306 $credsResponse = $httpHandler($credsRequest);
307 $awsCreds = json_decode((string) $credsResponse->getBody(), true);
308 return [
309 $awsCreds['AccessKeyId'], // accessKeyId
310 $awsCreds['SecretAccessKey'], // secretAccessKey
311 $awsCreds['Token'], // token
312 ];
313 }
314
315 /**
316 * @internal
317 *
318 * @param callable $httpHandler
319 * @return array{string, string, ?string}|null
320 */
321 public static function getSigningVarsFromEcs(callable $httpHandler): ?array
322 {
323 // Load the environment variables defined by AWS for the ECS/EKS container metadata.
324 $ecsContainerCredentialsRelativeUri = getenv('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI');
325 $ecsContainerCredentialsFullUri = getenv('AWS_CONTAINER_CREDENTIALS_FULL_URI');
326 $ecsContainerAuthorizationToken = getenv('AWS_CONTAINER_AUTHORIZATION_TOKEN');
327 $ecsContainerAuthorizationTokenFile = getenv('AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE');
328
329 $credentialsUrl = '';
330 // The full URI takes precedence over the relative URI if both are defined.
331 if ($ecsContainerCredentialsFullUri) {
332 $credentialsUrl = $ecsContainerCredentialsFullUri;
333 } elseif ($ecsContainerCredentialsRelativeUri) {
334 // The relative URI is appended to the default ECS Task Metadata Endpoint.
335 $credentialsUrl = self::ECS_CONTAINER_METADATA_URL . $ecsContainerCredentialsRelativeUri;
336 } else {
337 // Not running in an ECS environment, or metadata is not enabled.
338 return null;
339 }
340
341 $headers = [];
342 // The authorization token file takes precedence over the direct token variable.
343 if ($ecsContainerAuthorizationTokenFile) {
344 if (is_readable($ecsContainerAuthorizationTokenFile)) {
345 $headers['Authorization'] = trim((string) file_get_contents($ecsContainerAuthorizationTokenFile));
346 } else {
347 throw new \RuntimeException(
348 sprintf('Token file %s is not readable', $ecsContainerAuthorizationTokenFile)
349 );
350 }
351 } elseif ($ecsContainerAuthorizationToken) {
352 $headers['Authorization'] = $ecsContainerAuthorizationToken;
353 }
354
355 // Fetch the temporary AWS credentials from the resolved metadata endpoint.
356 $credsRequest = new Request('GET', $credentialsUrl, $headers);
357 $credsResponse = $httpHandler($credsRequest);
358 $awsCreds = json_decode((string) $credsResponse->getBody(), true);
359
360 // Ensure the response has the minimum required credential fields.
361 if (!is_array($awsCreds) || !isset($awsCreds['AccessKeyId']) || !isset($awsCreds['SecretAccessKey'])) {
362 throw new \UnexpectedValueException('Invalid or missing ECS credentials in response');
363 }
364
365 return [
366 $awsCreds['AccessKeyId'],
367 $awsCreds['SecretAccessKey'],
368 $awsCreds['Token'] ?? null,
369 ];
370 }
371
372 /**
373 * @internal
374 *
375 * @return array{string, string, ?string}
376 */
377 public static function getSigningVarsFromEnv(): ?array
378 {
379 $accessKeyId = getenv('AWS_ACCESS_KEY_ID');
380 $secretAccessKey = getenv('AWS_SECRET_ACCESS_KEY');
381 if ($accessKeyId && $secretAccessKey) {
382 return [
383 $accessKeyId,
384 $secretAccessKey,
385 getenv('AWS_SESSION_TOKEN') ?: null, // session token (can be null)
386 ];
387 }
388
389 return null;
390 }
391
392 /**
393 * Gets the unique key for caching
394 * For AwsNativeSource the values are:
395 * Imdsv2SessionTokenUrl.SecurityCredentialsUrl.RegionUrl.RegionalCredVerificationUrl
396 *
397 * @return string
398 */
399 public function getCacheKey(): string
400 {
401 return ($this->imdsv2SessionTokenUrl ?? '') .
402 '.' . ($this->securityCredentialsUrl ?? '') .
403 '.' . $this->regionUrl .
404 '.' . $this->regionalCredVerificationUrl;
405 }
406
407 /**
408 * Return HMAC hash in binary string
409 */
410 private static function hmacSign(string $key, string $msg): string
411 {
412 return hash_hmac('sha256', self::utf8Encode($msg), $key, true);
413 }
414
415 /**
416 * @TODO add a fallback when mbstring is not available
417 */
418 private static function utf8Encode(string $string): string
419 {
420 return (string) mb_convert_encoding($string, 'UTF-8', 'ISO-8859-1');
421 }
422
423 private static function getSignatureKey(
424 string $key,
425 string $dateStamp,
426 string $regionName,
427 string $serviceName
428 ): string {
429 $kDate = self::hmacSign(self::utf8Encode('AWS4' . $key), $dateStamp);
430 $kRegion = self::hmacSign($kDate, $regionName);
431 $kService = self::hmacSign($kRegion, $serviceName);
432 $kSigning = self::hmacSign($kService, 'aws4_request');
433
434 return $kSigning;
435 }
436 }
437