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 Google\Auth\CredentialsLoader; 21use Google\Auth\GetQuotaProjectInterface; 22use Google\Auth\OAuth2; 23use Google\Auth\ProjectIdProviderInterface; 24use Google\Auth\ServiceAccountSignerTrait; 25use Google\Auth\SignBlobInterface; 26use InvalidArgumentException; 27 28/** 29 * ServiceAccountCredentials supports authorization using a Google service 30 * account. 31 * 32 * (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount) 33 * 34 * It's initialized using the json key file that's downloadable from developer 35 * console, which should contain a private_key and client_email fields that it 36 * uses. 37 * 38 * Use it with AuthTokenMiddleware to authorize http requests: 39 * 40 * use Google\Auth\Credentials\ServiceAccountCredentials; 41 * use Google\Auth\Middleware\AuthTokenMiddleware; 42 * use GuzzleHttp\Client; 43 * use GuzzleHttp\HandlerStack; 44 * 45 * $sa = new ServiceAccountCredentials( 46 * 'https://www.googleapis.com/auth/taskqueue', 47 * '/path/to/your/json/key_file.json' 48 * ); 49 * $middleware = new AuthTokenMiddleware($sa); 50 * $stack = HandlerStack::create(); 51 * $stack->push($middleware); 52 * 53 * $client = new Client([ 54 * 'handler' => $stack, 55 * 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/', 56 * 'auth' => 'google_auth' // authorize all requests 57 * ]); 58 * 59 * $res = $client->get('myproject/taskqueues/myqueue'); 60 */ 61class ServiceAccountCredentials extends CredentialsLoader implements 62 GetQuotaProjectInterface, 63 SignBlobInterface, 64 ProjectIdProviderInterface 65{ 66 use ServiceAccountSignerTrait; 67 68 /** 69 * The OAuth2 instance used to conduct authorization. 70 * 71 * @var OAuth2 72 */ 73 protected $auth; 74 75 /** 76 * The quota project associated with the JSON credentials 77 * 78 * @var string 79 */ 80 protected $quotaProject; 81 82 /** 83 * @var string|null 84 */ 85 protected $projectId; 86 87 /** 88 * @var array<mixed>|null 89 */ 90 private $lastReceivedJwtAccessToken; 91 92 /** 93 * @var bool 94 */ 95 private $useJwtAccessWithScope = false; 96 97 /** 98 * @var ServiceAccountJwtAccessCredentials|null 99 */ 100 private $jwtAccessCredentials; 101 102 /** 103 * Create a new ServiceAccountCredentials. 104 * 105 * @param string|string[]|null $scope the scope of the access request, expressed 106 * either as an Array or as a space-delimited String. 107 * @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials 108 * as an associative array 109 * @param string $sub an email address account to impersonate, in situations when 110 * the service account has been delegated domain wide access. 111 * @param string $targetAudience The audience for the ID token. 112 */ 113 public function __construct( 114 $scope, 115 $jsonKey, 116 $sub = null, 117 $targetAudience = null 118 ) { 119 if (is_string($jsonKey)) { 120 if (!file_exists($jsonKey)) { 121 throw new \InvalidArgumentException('file does not exist'); 122 } 123 $jsonKeyStream = file_get_contents($jsonKey); 124 if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) { 125 throw new \LogicException('invalid json for auth config'); 126 } 127 } 128 if (!array_key_exists('client_email', $jsonKey)) { 129 throw new \InvalidArgumentException( 130 'json key is missing the client_email field' 131 ); 132 } 133 if (!array_key_exists('private_key', $jsonKey)) { 134 throw new \InvalidArgumentException( 135 'json key is missing the private_key field' 136 ); 137 } 138 if (array_key_exists('quota_project_id', $jsonKey)) { 139 $this->quotaProject = (string) $jsonKey['quota_project_id']; 140 } 141 if ($scope && $targetAudience) { 142 throw new InvalidArgumentException( 143 'Scope and targetAudience cannot both be supplied' 144 ); 145 } 146 $additionalClaims = []; 147 if ($targetAudience) { 148 $additionalClaims = ['target_audience' => $targetAudience]; 149 } 150 $this->auth = new OAuth2([ 151 'audience' => self::TOKEN_CREDENTIAL_URI, 152 'issuer' => $jsonKey['client_email'], 153 'scope' => $scope, 154 'signingAlgorithm' => 'RS256', 155 'signingKey' => $jsonKey['private_key'], 156 'sub' => $sub, 157 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 158 'additionalClaims' => $additionalClaims, 159 ]); 160 161 $this->projectId = isset($jsonKey['project_id']) 162 ? $jsonKey['project_id'] 163 : null; 164 } 165 166 /** 167 * When called, the ServiceAccountCredentials will use an instance of 168 * ServiceAccountJwtAccessCredentials to fetch (self-sign) an access token 169 * even when only scopes are supplied. Otherwise, 170 * ServiceAccountJwtAccessCredentials is only called when no scopes and an 171 * authUrl (audience) is suppled. 172 * 173 * @return void 174 */ 175 public function useJwtAccessWithScope() 176 { 177 $this->useJwtAccessWithScope = true; 178 } 179 180 /** 181 * @param callable $httpHandler 182 * 183 * @return array<mixed> { 184 * A set of auth related metadata, containing the following 185 * 186 * @type string $access_token 187 * @type int $expires_in 188 * @type string $token_type 189 * } 190 */ 191 public function fetchAuthToken(callable $httpHandler = null) 192 { 193 if ($this->useSelfSignedJwt()) { 194 $jwtCreds = $this->createJwtAccessCredentials(); 195 196 $accessToken = $jwtCreds->fetchAuthToken($httpHandler); 197 198 if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) { 199 // Keep self-signed JWTs in memory as the last received token 200 $this->lastReceivedJwtAccessToken = $lastReceivedToken; 201 } 202 203 return $accessToken; 204 } 205 return $this->auth->fetchAuthToken($httpHandler); 206 } 207 208 /** 209 * @return string 210 */ 211 public function getCacheKey() 212 { 213 $key = $this->auth->getIssuer() . ':' . $this->auth->getCacheKey(); 214 if ($sub = $this->auth->getSub()) { 215 $key .= ':' . $sub; 216 } 217 218 return $key; 219 } 220 221 /** 222 * @return array<mixed> 223 */ 224 public function getLastReceivedToken() 225 { 226 // If self-signed JWTs are being used, fetch the last received token 227 // from memory. Else, fetch it from OAuth2 228 return $this->useSelfSignedJwt() 229 ? $this->lastReceivedJwtAccessToken 230 : $this->auth->getLastReceivedToken(); 231 } 232 233 /** 234 * Get the project ID from the service account keyfile. 235 * 236 * Returns null if the project ID does not exist in the keyfile. 237 * 238 * @param callable $httpHandler Not used by this credentials type. 239 * @return string|null 240 */ 241 public function getProjectId(callable $httpHandler = null) 242 { 243 return $this->projectId; 244 } 245 246 /** 247 * Updates metadata with the authorization token. 248 * 249 * @param array<mixed> $metadata metadata hashmap 250 * @param string $authUri optional auth uri 251 * @param callable $httpHandler callback which delivers psr7 request 252 * @return array<mixed> updated metadata hashmap 253 */ 254 public function updateMetadata( 255 $metadata, 256 $authUri = null, 257 callable $httpHandler = null 258 ) { 259 // scope exists. use oauth implementation 260 if (!$this->useSelfSignedJwt()) { 261 return parent::updateMetadata($metadata, $authUri, $httpHandler); 262 } 263 264 $jwtCreds = $this->createJwtAccessCredentials(); 265 if ($this->auth->getScope()) { 266 // Prefer user-provided "scope" to "audience" 267 $updatedMetadata = $jwtCreds->updateMetadata($metadata, null, $httpHandler); 268 } else { 269 $updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler); 270 } 271 272 if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) { 273 // Keep self-signed JWTs in memory as the last received token 274 $this->lastReceivedJwtAccessToken = $lastReceivedToken; 275 } 276 277 return $updatedMetadata; 278 } 279 280 /** 281 * @return ServiceAccountJwtAccessCredentials 282 */ 283 private function createJwtAccessCredentials() 284 { 285 if (!$this->jwtAccessCredentials) { 286 // Create credentials for self-signing a JWT (JwtAccess) 287 $credJson = [ 288 'private_key' => $this->auth->getSigningKey(), 289 'client_email' => $this->auth->getIssuer(), 290 ]; 291 $this->jwtAccessCredentials = new ServiceAccountJwtAccessCredentials( 292 $credJson, 293 $this->auth->getScope() 294 ); 295 } 296 297 return $this->jwtAccessCredentials; 298 } 299 300 /** 301 * @param string $sub an email address account to impersonate, in situations when 302 * the service account has been delegated domain wide access. 303 * @return void 304 */ 305 public function setSub($sub) 306 { 307 $this->auth->setSub($sub); 308 } 309 310 /** 311 * Get the client name from the keyfile. 312 * 313 * In this case, it returns the keyfile's client_email key. 314 * 315 * @param callable $httpHandler Not used by this credentials type. 316 * @return string 317 */ 318 public function getClientName(callable $httpHandler = null) 319 { 320 return $this->auth->getIssuer(); 321 } 322 323 /** 324 * Get the quota project used for this API request 325 * 326 * @return string|null 327 */ 328 public function getQuotaProject() 329 { 330 return $this->quotaProject; 331 } 332 333 /** 334 * @return bool 335 */ 336 private function useSelfSignedJwt() 337 { 338 // If claims are set, this call is for "id_tokens" 339 if ($this->auth->getAdditionalClaims()) { 340 return false; 341 } 342 343 // When true, ServiceAccountCredentials will always use JwtAccess for access tokens 344 if ($this->useJwtAccessWithScope) { 345 return true; 346 } 347 return is_null($this->auth->getScope()); 348 } 349} 350