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