PluginProbe
Gmail SMTP / trunk
Gmail SMTP vtrunk
1.2.3.21 1.2.3.20 trunk 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.9 1.2.0 1.2.3.14 1.2.3.15 1.2.3.16 1.2.3.18 1.2.3.5
gmail-smtp / google-api-php-client / vendor / google / auth / src / Credentials / ImpersonatedServiceAccountCredentials.php

ImpersonatedServiceAccountCredentials.php in Gmail SMTP trunk, at google-api-php-client/vendor/google/auth/src/Credentials/ImpersonatedServiceAccountCredentials.php

344 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * Copyright 2022 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 namespace Google\Auth\Credentials;
20
21 use Google\Auth\CacheTrait;
22 use Google\Auth\CredentialsLoader;
23 use Google\Auth\FetchAuthTokenInterface;
24 use Google\Auth\GetUniverseDomainInterface;
25 use Google\Auth\HttpHandler\HttpClientCache;
26 use Google\Auth\HttpHandler\HttpHandlerFactory;
27 use Google\Auth\IamSignerTrait;
28 use Google\Auth\SignBlobInterface;
29 use Google\Auth\UpdateMetadataInterface;
30 use Google\Auth\UpdateMetadataTrait;
31 use GuzzleHttp\Psr7\Request;
32 use InvalidArgumentException;
33 use LogicException;
34
35 /**
36 * **IMPORTANT**:
37 * This class does not validate the credential configuration. A security
38 * risk occurs when a credential configuration configured with malicious urls
39 * is used.
40 * When the credential configuration is accepted from an
41 * untrusted source, you should validate it before creating this class.
42 * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
43 */
44 class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
45 SignBlobInterface,
46 GetUniverseDomainInterface,
47 UpdateMetadataInterface
48 {
49 use CacheTrait;
50 use IamSignerTrait;
51 use UpdateMetadataTrait;
52 use RegionalAccessBoundaryTrait;
53
54 private const CRED_TYPE = 'imp';
55 private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
56 private const ID_TOKEN_IMPERSONATION_URL =
57 'https://iamcredentials.UNIVERSE_DOMAIN/v1/projects/-/serviceAccounts/%s:generateIdToken';
58
59 /**
60 * @var string
61 */
62 protected $impersonatedServiceAccountName;
63
64 protected FetchAuthTokenInterface $sourceCredentials;
65
66 private string $serviceAccountImpersonationUrl;
67
68 /**
69 * @var string[]
70 */
71 private array $delegates;
72
73 /**
74 * @var string|string[]
75 */
76 private string|array $targetScope;
77
78 private int $lifetime;
79
80 /**
81 * @var array<mixed>|null
82 */
83 protected array|null $lastReceivedToken = null;
84
85 /**
86 * Instantiate an instance of ImpersonatedServiceAccountCredentials from a credentials file that
87 * has be created with the --impersonate-service-account flag.
88 *
89 * @param string|string[]|null $scope The scope of the access request, expressed either as an
90 * array or as a space-delimited string.
91 * @param string|array<mixed> $jsonKey JSON credential file path or JSON array credentials {
92 * JSON credentials as an associative array.
93 *
94 * @type string $service_account_impersonation_url The URL to the service account
95 * @type string|FetchAuthTokenInterface $source_credentials The source credentials to impersonate
96 * @type int $lifetime The lifetime of the impersonated credentials
97 * @type string[] $delegates The delegates to impersonate
98 * }
99 * @param string|null $targetAudience The audience to request an ID token.
100 * @param string|string[]|null $defaultScope The scopes to be used if no "scopes" field exists
101 * in the `$jsonKey`.
102 */
103 public function __construct(
104 string|array|null $scope,
105 string|array $jsonKey,
106 private ?string $targetAudience = null,
107 string|array|null $defaultScope = null,
108 bool $enableRegionalAccessBoundary = false
109 ) {
110 if (is_string($jsonKey)) {
111 if (!file_exists($jsonKey)) {
112 throw new InvalidArgumentException('file does not exist');
113 }
114 $json = file_get_contents($jsonKey);
115 if (!$jsonKey = json_decode((string) $json, true)) {
116 throw new LogicException('invalid json for auth config');
117 }
118 }
119 if (!array_key_exists('service_account_impersonation_url', $jsonKey)) {
120 throw new LogicException(
121 'json key is missing the service_account_impersonation_url field'
122 );
123 }
124 if (!array_key_exists('source_credentials', $jsonKey)) {
125 throw new LogicException('json key is missing the source_credentials field');
126 }
127
128 $jsonKeyScope = $jsonKey['scopes'] ?? null;
129 $scope = $scope ?: $jsonKeyScope ?: $defaultScope;
130 if ($scope && $targetAudience) {
131 throw new InvalidArgumentException(
132 'Scope and targetAudience cannot both be supplied'
133 );
134 }
135 if (is_array($jsonKey['source_credentials'])) {
136 if (!array_key_exists('type', $jsonKey['source_credentials'])) {
137 throw new InvalidArgumentException('json key source credentials are missing the type field');
138 }
139 if (
140 $targetAudience !== null
141 && $jsonKey['source_credentials']['type'] === 'service_account'
142 ) {
143 // Service account tokens MUST request a scope, and as this token is only used to impersonate
144 // an ID token, the narrowest scope we can request is `iam`.
145 $scope = self::IAM_SCOPE;
146 }
147 $jsonKey['source_credentials'] = match ($jsonKey['source_credentials']['type'] ?? null) {
148 // Do not pass $defaultScope to ServiceAccountCredentials
149 'service_account' => new ServiceAccountCredentials(
150 scope: $scope,
151 jsonKey: $jsonKey['source_credentials'],
152 ),
153 'authorized_user' => new UserRefreshCredentials($scope, $jsonKey['source_credentials']),
154 'external_account' => new ExternalAccountCredentials($scope, $jsonKey['source_credentials']),
155 default => throw new \InvalidArgumentException('invalid value in the type field'),
156 };
157 }
158
159 $this->targetScope = $scope ?? [];
160 $this->lifetime = $jsonKey['lifetime'] ?? 3600;
161 $this->delegates = $jsonKey['delegates'] ?? [];
162
163 $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'];
164 $this->impersonatedServiceAccountName = $this->getImpersonatedServiceAccountNameFromUrl(
165 $this->serviceAccountImpersonationUrl
166 );
167
168 $this->sourceCredentials = $jsonKey['source_credentials'];
169 $this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;
170 }
171
172 /**
173 * Helper function for extracting the Server Account Name from the URL saved in the account
174 * credentials file.
175 *
176 * @param $serviceAccountImpersonationUrl string URL from "service_account_impersonation_url"
177 * @return string Service account email or ID.
178 */
179 private function getImpersonatedServiceAccountNameFromUrl(
180 string $serviceAccountImpersonationUrl
181 ): string {
182 $fields = explode('/', $serviceAccountImpersonationUrl);
183 $lastField = end($fields);
184 $splitter = explode(':', $lastField);
185 return $splitter[0];
186 }
187
188 /**
189 * Get the client name from the keyfile
190 *
191 * In this implementation, it will return the issuers email from the oauth token.
192 *
193 * @param callable|null $unusedHttpHandler not used by this credentials type.
194 * @return string Token issuer email
195 */
196 public function getClientName(?callable $unusedHttpHandler = null)
197 {
198 return $this->impersonatedServiceAccountName;
199 }
200
201 /**
202 * @param callable|null $httpHandler
203 *
204 * @return array<mixed> {
205 * A set of auth related metadata, containing the following
206 *
207 * @type string $access_token
208 * @type int $expires_in
209 * @type string $scope
210 * @type string $token_type
211 * @type string $id_token
212 * }
213 */
214 public function fetchAuthToken(?callable $httpHandler = null)
215 {
216 $httpHandler = $httpHandler ?? HttpHandlerFactory::build(HttpClientCache::getHttpClient());
217
218 // The FetchAuthTokenInterface technically does not have a "headers" argument, but all of
219 // the implementations do. Additionally, passing in more parameters than the function has
220 // defined is allowed in PHP. So we'll just ignore the phpstan error here.
221 // @phpstan-ignore-next-line
222 $authToken = $this->sourceCredentials->fetchAuthToken(
223 $httpHandler,
224 $this->applyTokenEndpointMetrics([], 'at')
225 );
226
227 $headers = $this->applyTokenEndpointMetrics([
228 'Content-Type' => 'application/json',
229 'Cache-Control' => 'no-store',
230 'Authorization' => sprintf('Bearer %s', $authToken['access_token'] ?? $authToken['id_token']),
231 ], $this->isIdTokenRequest() ? 'it' : 'at');
232
233 $body = match ($this->isIdTokenRequest()) {
234 true => [
235 'audience' => $this->targetAudience,
236 'includeEmail' => true,
237 ],
238 false => [
239 'scope' => $this->targetScope,
240 'delegates' => $this->delegates,
241 'lifetime' => sprintf('%ss', $this->lifetime),
242 ]
243 };
244
245 $url = $this->serviceAccountImpersonationUrl;
246 if ($this->isIdTokenRequest()) {
247 $regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
248 if (!preg_match($regex, $url, $matches)) {
249 throw new InvalidArgumentException(
250 'Invalid service account impersonation URL - unable to parse service account email'
251 );
252 }
253 $url = str_replace(
254 'UNIVERSE_DOMAIN',
255 $this->getUniverseDomain(),
256 sprintf(self::ID_TOKEN_IMPERSONATION_URL, $matches['email'])
257 );
258 }
259
260 $request = new Request(
261 'POST',
262 $url,
263 $headers,
264 (string) json_encode($body)
265 );
266
267 $response = $httpHandler($request);
268 $body = json_decode((string) $response->getBody(), true);
269
270 return $this->lastReceivedToken = match ($this->isIdTokenRequest()) {
271 true => ['id_token' => $body['token']],
272 false => [
273 'access_token' => $body['accessToken'],
274 'expires_at' => strtotime($body['expireTime']),
275 ]
276 };
277 }
278
279 /**
280 * Returns the Cache Key for the credentials
281 * The cache key is the same as the UserRefreshCredentials class
282 *
283 * @return string
284 */
285 public function getCacheKey()
286 {
287 return $this->getFullCacheKey(
288 $this->serviceAccountImpersonationUrl . $this->sourceCredentials->getCacheKey()
289 );
290 }
291
292 /**
293 * @return array<mixed>
294 */
295 public function getLastReceivedToken()
296 {
297 return $this->lastReceivedToken;
298 }
299
300 protected function getCredType(): string
301 {
302 return self::CRED_TYPE;
303 }
304
305 private function isIdTokenRequest(): bool
306 {
307 return !is_null($this->targetAudience);
308 }
309
310 public function getUniverseDomain(): string
311 {
312 return $this->sourceCredentials instanceof GetUniverseDomainInterface
313 ? $this->sourceCredentials->getUniverseDomain()
314 : self::DEFAULT_UNIVERSE_DOMAIN;
315 }
316
317 /**
318 * Updates metadata with the authorization token.
319 *
320 * @param array<mixed> $metadata metadata hashmap
321 * @param string $authUri optional auth uri
322 * @param callable|null $httpHandler callback which delivers psr7 request
323 * @return array<mixed> updated metadata hashmap
324 */
325 public function updateMetadata(
326 $metadata,
327 $authUri = null,
328 ?callable $httpHandler = null
329 ) {
330 $metatadata = parent::updateMetadata($metadata, $authUri, $httpHandler);
331
332 $metatadata = $this->updateRegionalAccessBoundaryMetadata(
333 $metatadata,
334 $this->buildRegionalAccessBoundaryLookupUrl(
335 serviceAccountEmail: $this->impersonatedServiceAccountName
336 ),
337 $this->getUniverseDomain(),
338 $httpHandler,
339 );
340
341 return $metatadata;
342 }
343 }
344