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 / ServiceAccountCredentials.php

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

493 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Copyright 2015 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\Credentials;
19
20 use Firebase\JWT\JWT;
21 use Google\Auth\CredentialsLoader;
22 use Google\Auth\GetQuotaProjectInterface;
23 use Google\Auth\HttpHandler\HttpClientCache;
24 use Google\Auth\HttpHandler\HttpHandlerFactory;
25 use Google\Auth\Iam;
26 use Google\Auth\OAuth2;
27 use Google\Auth\ProjectIdProviderInterface;
28 use Google\Auth\ServiceAccountSignerTrait;
29 use Google\Auth\SignBlobInterface;
30 use InvalidArgumentException;
31
32 /**
33 * ServiceAccountCredentials supports authorization using a Google service
34 * account.
35 *
36 * (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
37 *
38 * It's initialized using the json key file that's downloadable from developer
39 * console, which should contain a private_key and client_email fields that it
40 * uses.
41 *
42 * Use it with AuthTokenMiddleware to authorize http requests:
43 *
44 * ```
45 * use Google\Auth\Credentials\ServiceAccountCredentials;
46 * use Google\Auth\Middleware\AuthTokenMiddleware;
47 * use GuzzleHttp\Client;
48 * use GuzzleHttp\HandlerStack;
49 *
50 * $sa = new ServiceAccountCredentials(
51 * 'https://www.googleapis.com/auth/taskqueue',
52 * '/path/to/your/json/key_file.json'
53 * );
54 * $middleware = new AuthTokenMiddleware($sa);
55 * $stack = HandlerStack::create();
56 * $stack->push($middleware);
57 *
58 * $client = new Client([
59 * 'handler' => $stack,
60 * 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
61 * 'auth' => 'google_auth' // authorize all requests
62 * ]);
63 *
64 * $res = $client->get('myproject/taskqueues/myqueue');
65 * ```
66 */
67 class ServiceAccountCredentials extends CredentialsLoader implements
68 GetQuotaProjectInterface,
69 SignBlobInterface,
70 ProjectIdProviderInterface
71 {
72 use ServiceAccountSignerTrait;
73 use RegionalAccessBoundaryTrait;
74
75 /**
76 * Used in observability metric headers
77 *
78 * @var string
79 */
80 private const CRED_TYPE = 'sa';
81 private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
82
83 /**
84 * The OAuth2 instance used to conduct authorization.
85 *
86 * @var OAuth2
87 */
88 protected $auth;
89
90 /**
91 * The quota project associated with the JSON credentials
92 *
93 * @var string
94 */
95 protected $quotaProject;
96
97 /**
98 * @var string|null
99 */
100 protected $projectId;
101
102 /**
103 * @var array<mixed>|null
104 */
105 private $lastReceivedJwtAccessToken;
106
107 /**
108 * @var bool
109 */
110 private $useJwtAccessWithScope = false;
111
112 /**
113 * @var ServiceAccountJwtAccessCredentials|null
114 */
115 private $jwtAccessCredentials;
116
117 /**
118 * @var string
119 */
120 private string $universeDomain;
121
122 /**
123 * Whether this is an ID token request or an access token request. Used when
124 * building the metric header.
125 */
126 private bool $isIdTokenRequest = false;
127
128 /**
129 * Create a new ServiceAccountCredentials.
130 *
131 * @param string|string[]|null $scope the scope of the access request, expressed
132 * either as an Array or as a space-delimited String.
133 * @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
134 * as an associative array
135 * @param string $sub an email address account to impersonate, in situations when
136 * the service account has been delegated domain wide access.
137 * @param string $targetAudience The audience for the ID token.
138 * @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
139 */
140 public function __construct(
141 $scope,
142 $jsonKey,
143 $sub = null,
144 $targetAudience = null,
145 bool $enableRegionalAccessBoundary = false
146 ) {
147 if (is_string($jsonKey)) {
148 if (!file_exists($jsonKey)) {
149 throw new \InvalidArgumentException('file does not exist');
150 }
151 $jsonKeyStream = file_get_contents($jsonKey);
152 if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
153 throw new \LogicException('invalid json for auth config');
154 }
155 }
156 if (!array_key_exists('client_email', $jsonKey)) {
157 throw new \InvalidArgumentException(
158 'json key is missing the client_email field'
159 );
160 }
161 if (!array_key_exists('private_key', $jsonKey)) {
162 throw new \InvalidArgumentException(
163 'json key is missing the private_key field'
164 );
165 }
166 if (array_key_exists('quota_project_id', $jsonKey)) {
167 $this->quotaProject = (string) $jsonKey['quota_project_id'];
168 }
169 if ($scope && $targetAudience) {
170 throw new InvalidArgumentException(
171 'Scope and targetAudience cannot both be supplied'
172 );
173 }
174 $additionalClaims = [];
175 if ($targetAudience) {
176 $additionalClaims = ['target_audience' => $targetAudience];
177 $this->isIdTokenRequest = true;
178 }
179 $this->auth = new OAuth2([
180 'audience' => self::TOKEN_CREDENTIAL_URI,
181 'issuer' => $jsonKey['client_email'],
182 'scope' => $scope,
183 'signingAlgorithm' => 'RS256',
184 'signingKey' => $jsonKey['private_key'],
185 'signingKeyId' => $jsonKey['private_key_id'] ?? null,
186 'sub' => $sub,
187 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
188 'additionalClaims' => $additionalClaims,
189 ]);
190
191 $this->projectId = $jsonKey['project_id'] ?? null;
192 $this->universeDomain = $jsonKey['universe_domain'] ?? self::DEFAULT_UNIVERSE_DOMAIN;
193 $this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;
194 }
195
196 /**
197 * When called, the ServiceAccountCredentials will use an instance of
198 * ServiceAccountJwtAccessCredentials to fetch (self-sign) an access token
199 * even when only scopes are supplied. Otherwise,
200 * ServiceAccountJwtAccessCredentials is only called when no scopes and an
201 * authUrl (audience) is suppled.
202 *
203 * @return void
204 */
205 public function useJwtAccessWithScope()
206 {
207 $this->useJwtAccessWithScope = true;
208 }
209
210 /**
211 * @param callable|null $httpHandler
212 * @param array<mixed> $headers [optional] Headers to be inserted
213 * into the token endpoint request present.
214 *
215 * @return array<mixed> {
216 * A set of auth related metadata, containing the following
217 *
218 * @type string $access_token
219 * @type int $expires_in
220 * @type string $token_type
221 * }
222 */
223 public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
224 {
225 $httpHandler = $httpHandler
226 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
227
228 if ($this->useSelfSignedJwt()) {
229 $jwtCreds = $this->createJwtAccessCredentials();
230 $accessToken = $jwtCreds->fetchAuthToken($httpHandler);
231
232 if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
233 // Keep self-signed JWTs in memory as the last received token
234 $this->lastReceivedJwtAccessToken = $lastReceivedToken;
235 }
236
237 return $accessToken;
238 }
239
240 if ($this->isIdTokenRequest && $this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
241 $now = time();
242 $jwt = Jwt::encode(
243 [
244 'iss' => $this->auth->getIssuer(),
245 'sub' => $this->auth->getIssuer(),
246 'scope' => self::IAM_SCOPE,
247 'exp' => ($now + $this->auth->getExpiry()),
248 'iat' => ($now - OAuth2::DEFAULT_SKEW_SECONDS),
249 ],
250 $this->auth->getSigningKey(),
251 $this->auth->getSigningAlgorithm(),
252 $this->auth->getSigningKeyId()
253 );
254 // We create a new instance of Iam each time because the `$httpHandler` might change.
255 $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken(
256 $this->auth->getIssuer(),
257 $this->auth->getAdditionalClaims()['target_audience'],
258 $jwt,
259 $this->applyTokenEndpointMetrics($headers, 'it')
260 );
261 return ['id_token' => $idToken];
262 }
263 return $this->auth->fetchAuthToken(
264 $httpHandler,
265 $this->applyTokenEndpointMetrics($headers, $this->isIdTokenRequest ? 'it' : 'at')
266 );
267 }
268
269 /**
270 * Return the Cache Key for the credentials.
271 * For the cache key format is one of the following:
272 * ClientEmail.Scope[.Sub]
273 * ClientEmail.Audience[.Sub]
274 *
275 * @return string
276 */
277 public function getCacheKey()
278 {
279 $scopeOrAudience = $this->auth->getScope();
280 if (!$scopeOrAudience) {
281 $scopeOrAudience = $this->auth->getAudience();
282 }
283
284 $key = $this->auth->getIssuer() . '.' . $scopeOrAudience;
285 if ($sub = $this->auth->getSub()) {
286 $key .= '.' . $sub;
287 }
288
289 return $key;
290 }
291
292 /**
293 * @return array<mixed>
294 */
295 public function getLastReceivedToken()
296 {
297 // If self-signed JWTs are being used, fetch the last received token
298 // from memory. Else, fetch it from OAuth2
299 return $this->useSelfSignedJwt()
300 ? $this->lastReceivedJwtAccessToken
301 : $this->auth->getLastReceivedToken();
302 }
303
304 /**
305 * Get the project ID from the service account keyfile.
306 *
307 * Returns null if the project ID does not exist in the keyfile.
308 *
309 * @param callable|null $httpHandler Not used by this credentials type.
310 * @return string|null
311 */
312 public function getProjectId(?callable $httpHandler = null)
313 {
314 return $this->projectId;
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 $metadata = $this->useSelfSignedJwt()
331 ? $this->updateMetadataSelfSignedJwt($metadata, $authUri, $httpHandler)
332 : parent::updateMetadata($metadata, $authUri, $httpHandler);
333
334 $metadata = $this->updateRegionalAccessBoundaryMetadata(
335 $metadata,
336 $this->buildRegionalAccessBoundaryLookupUrl(
337 serviceAccountEmail: $this->auth->getIssuer()
338 ),
339 $this->getUniverseDomain(),
340 $httpHandler,
341 );
342
343 return $metadata;
344 }
345
346 /**
347 * Updates metadata with the authorization token for SSJWTs.
348 *
349 * @param array<mixed> $metadata metadata hashmap
350 * @param string $authUri optional auth uri
351 * @param callable|null $httpHandler callback which delivers psr7 request
352 * @return array<mixed> updated metadata hashmap
353 */
354 private function updateMetadataSelfSignedJwt(
355 $metadata,
356 $authUri = null,
357 ?callable $httpHandler = null
358 ) {
359 $jwtCreds = $this->createJwtAccessCredentials();
360
361 $metadata = $jwtCreds->updateMetadata(
362 $metadata,
363 // Prefer user-provided "scope" to "audience"
364 $this->auth->getScope() ? null : $authUri,
365 $httpHandler
366 );
367
368 if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
369 // Keep self-signed JWTs in memory as the last received token
370 $this->lastReceivedJwtAccessToken = $lastReceivedToken;
371 }
372
373 return $metadata;
374 }
375
376 /**
377 * @return ServiceAccountJwtAccessCredentials
378 */
379 private function createJwtAccessCredentials()
380 {
381 if (!$this->jwtAccessCredentials) {
382 // Create credentials for self-signing a JWT (JwtAccess)
383 $credJson = [
384 'private_key' => $this->auth->getSigningKey(),
385 'client_email' => $this->auth->getIssuer(),
386 ];
387 $this->jwtAccessCredentials = new ServiceAccountJwtAccessCredentials(
388 $credJson,
389 $this->auth->getScope()
390 );
391 }
392
393 return $this->jwtAccessCredentials;
394 }
395
396 /**
397 * @param string $sub an email address account to impersonate, in situations when
398 * the service account has been delegated domain wide access.
399 * @return void
400 */
401 public function setSub($sub)
402 {
403 $this->auth->setSub($sub);
404 }
405
406 /**
407 * Get the client name from the keyfile.
408 *
409 * In this case, it returns the keyfile's client_email key.
410 *
411 * @param callable|null $httpHandler Not used by this credentials type.
412 * @return string
413 */
414 public function getClientName(?callable $httpHandler = null)
415 {
416 return $this->auth->getIssuer();
417 }
418
419 /**
420 * Get the private key from the keyfile.
421 *
422 * In this case, it returns the keyfile's private_key key, needed for JWT signing.
423 *
424 * @return string
425 */
426 public function getPrivateKey()
427 {
428 return $this->auth->getSigningKey();
429 }
430
431 /**
432 * Get the quota project used for this API request
433 *
434 * @return string|null
435 */
436 public function getQuotaProject()
437 {
438 return $this->quotaProject;
439 }
440
441 /**
442 * Get the universe domain configured in the JSON credential.
443 *
444 * @return string
445 */
446 public function getUniverseDomain(): string
447 {
448 return $this->universeDomain;
449 }
450
451 protected function getCredType(): string
452 {
453 return self::CRED_TYPE;
454 }
455
456 /**
457 * @return bool
458 */
459 private function useSelfSignedJwt()
460 {
461 // When a sub is supplied, the user is using domain-wide delegation, which not available
462 // with self-signed JWTs
463 if (null !== $this->auth->getSub()) {
464 // If we are outside the GDU, we can't use domain-wide delegation
465 if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
466 throw new \LogicException(sprintf(
467 'Service Account subject is configured for the credential. Domain-wide ' .
468 'delegation is not supported in universes other than %s.',
469 self::DEFAULT_UNIVERSE_DOMAIN
470 ));
471 }
472 return false;
473 }
474
475 // Do not use self-signed JWT for ID tokens
476 if ($this->isIdTokenRequest) {
477 return false;
478 }
479
480 // When true, ServiceAccountCredentials will always use JwtAccess for access tokens
481 if ($this->useJwtAccessWithScope) {
482 return true;
483 }
484
485 // If the universe domain is outside the GDU, use JwtAccess for access tokens
486 if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
487 return true;
488 }
489
490 return is_null($this->auth->getScope());
491 }
492 }
493