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|null 89 */ 90 private $lastReceivedJwtAccessToken; 91 92 /** 93 * Create a new ServiceAccountCredentials. 94 * 95 * @param string|array $scope the scope of the access request, expressed 96 * either as an Array or as a space-delimited String. 97 * @param string|array $jsonKey JSON credential file path or JSON credentials 98 * as an associative array 99 * @param string $sub an email address account to impersonate, in situations when 100 * the service account has been delegated domain wide access. 101 * @param string $targetAudience The audience for the ID token. 102 */ 103 public function __construct( 104 $scope, 105 $jsonKey, 106 $sub = null, 107 $targetAudience = null 108 ) { 109 if (is_string($jsonKey)) { 110 if (!file_exists($jsonKey)) { 111 throw new \InvalidArgumentException('file does not exist'); 112 } 113 $jsonKeyStream = file_get_contents($jsonKey); 114 if (!$jsonKey = json_decode($jsonKeyStream, true)) { 115 throw new \LogicException('invalid json for auth config'); 116 } 117 } 118 if (!array_key_exists('client_email', $jsonKey)) { 119 throw new \InvalidArgumentException( 120 'json key is missing the client_email field' 121 ); 122 } 123 if (!array_key_exists('private_key', $jsonKey)) { 124 throw new \InvalidArgumentException( 125 'json key is missing the private_key field' 126 ); 127 } 128 if (array_key_exists('quota_project_id', $jsonKey)) { 129 $this->quotaProject = (string) $jsonKey['quota_project_id']; 130 } 131 if ($scope && $targetAudience) { 132 throw new InvalidArgumentException( 133 'Scope and targetAudience cannot both be supplied' 134 ); 135 } 136 $additionalClaims = []; 137 if ($targetAudience) { 138 $additionalClaims = ['target_audience' => $targetAudience]; 139 } 140 $this->auth = new OAuth2([ 141 'audience' => self::TOKEN_CREDENTIAL_URI, 142 'issuer' => $jsonKey['client_email'], 143 'scope' => $scope, 144 'signingAlgorithm' => 'RS256', 145 'signingKey' => $jsonKey['private_key'], 146 'sub' => $sub, 147 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 148 'additionalClaims' => $additionalClaims, 149 ]); 150 151 $this->projectId = isset($jsonKey['project_id']) 152 ? $jsonKey['project_id'] 153 : null; 154 } 155 156 /** 157 * @param callable $httpHandler 158 * 159 * @return array A set of auth related metadata, containing the following 160 * keys: 161 * - access_token (string) 162 * - expires_in (int) 163 * - token_type (string) 164 */ 165 public function fetchAuthToken(callable $httpHandler = null) 166 { 167 return $this->auth->fetchAuthToken($httpHandler); 168 } 169 170 /** 171 * @return string 172 */ 173 public function getCacheKey() 174 { 175 $key = $this->auth->getIssuer() . ':' . $this->auth->getCacheKey(); 176 if ($sub = $this->auth->getSub()) { 177 $key .= ':' . $sub; 178 } 179 180 return $key; 181 } 182 183 /** 184 * @return array 185 */ 186 public function getLastReceivedToken() 187 { 188 // If self-signed JWTs are being used, fetch the last received token 189 // from memory. Else, fetch it from OAuth2 190 return $this->useSelfSignedJwt() 191 ? $this->lastReceivedJwtAccessToken 192 : $this->auth->getLastReceivedToken(); 193 } 194 195 /** 196 * Get the project ID from the service account keyfile. 197 * 198 * Returns null if the project ID does not exist in the keyfile. 199 * 200 * @param callable $httpHandler Not used by this credentials type. 201 * @return string|null 202 */ 203 public function getProjectId(callable $httpHandler = null) 204 { 205 return $this->projectId; 206 } 207 208 /** 209 * Updates metadata with the authorization token. 210 * 211 * @param array $metadata metadata hashmap 212 * @param string $authUri optional auth uri 213 * @param callable $httpHandler callback which delivers psr7 request 214 * @return array updated metadata hashmap 215 */ 216 public function updateMetadata( 217 $metadata, 218 $authUri = null, 219 callable $httpHandler = null 220 ) { 221 // scope exists. use oauth implementation 222 if (!$this->useSelfSignedJwt()) { 223 return parent::updateMetadata($metadata, $authUri, $httpHandler); 224 } 225 226 // no scope found. create jwt with the auth uri 227 $credJson = array( 228 'private_key' => $this->auth->getSigningKey(), 229 'client_email' => $this->auth->getIssuer(), 230 ); 231 $jwtCreds = new ServiceAccountJwtAccessCredentials($credJson); 232 233 $updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler); 234 235 if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) { 236 // Keep self-signed JWTs in memory as the last received token 237 $this->lastReceivedJwtAccessToken = $lastReceivedToken; 238 } 239 240 return $updatedMetadata; 241 } 242 243 /** 244 * @param string $sub an email address account to impersonate, in situations when 245 * the service account has been delegated domain wide access. 246 */ 247 public function setSub($sub) 248 { 249 $this->auth->setSub($sub); 250 } 251 252 /** 253 * Get the client name from the keyfile. 254 * 255 * In this case, it returns the keyfile's client_email key. 256 * 257 * @param callable $httpHandler Not used by this credentials type. 258 * @return string 259 */ 260 public function getClientName(callable $httpHandler = null) 261 { 262 return $this->auth->getIssuer(); 263 } 264 265 /** 266 * Get the quota project used for this API request 267 * 268 * @return string|null 269 */ 270 public function getQuotaProject() 271 { 272 return $this->quotaProject; 273 } 274 275 private function useSelfSignedJwt() 276 { 277 return is_null($this->auth->getScope()); 278 } 279} 280