source: vendor/google/auth/src/Credentials/ServiceAccountCredentials.php@ f9c482b

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

Upload new project files

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