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; 19 20use Google\Auth\Credentials\InsecureCredentials; 21use Google\Auth\Credentials\ServiceAccountCredentials; 22use Google\Auth\Credentials\UserRefreshCredentials; 23use RuntimeException; 24use UnexpectedValueException; 25 26/** 27 * CredentialsLoader contains the behaviour used to locate and find default 28 * credentials files on the file system. 29 */ 30abstract class CredentialsLoader implements 31 FetchAuthTokenInterface, 32 UpdateMetadataInterface 33{ 34 const TOKEN_CREDENTIAL_URI = 'https://oauth2.googleapis.com/token'; 35 const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'; 36 const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'; 37 const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config'; 38 const MTLS_WELL_KNOWN_PATH = '.secureConnect/context_aware_metadata.json'; 39 const MTLS_CERT_ENV_VAR = 'GOOGLE_API_USE_CLIENT_CERTIFICATE'; 40 41 /** 42 * @param string $cause 43 * @return string 44 */ 45 private static function unableToReadEnv($cause) 46 { 47 $msg = 'Unable to read the credential file specified by '; 48 $msg .= ' GOOGLE_APPLICATION_CREDENTIALS: '; 49 $msg .= $cause; 50 51 return $msg; 52 } 53 54 /** 55 * @return bool 56 */ 57 private static function isOnWindows() 58 { 59 return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 60 } 61 62 /** 63 * Load a JSON key from the path specified in the environment. 64 * 65 * Load a JSON key from the path specified in the environment 66 * variable GOOGLE_APPLICATION_CREDENTIALS. Return null if 67 * GOOGLE_APPLICATION_CREDENTIALS is not specified. 68 * 69 * @return array<mixed>|null JSON key | null 70 */ 71 public static function fromEnv() 72 { 73 $path = getenv(self::ENV_VAR); 74 if (empty($path)) { 75 return null; 76 } 77 if (!file_exists($path)) { 78 $cause = 'file ' . $path . ' does not exist'; 79 throw new \DomainException(self::unableToReadEnv($cause)); 80 } 81 $jsonKey = file_get_contents($path); 82 return json_decode((string) $jsonKey, true); 83 } 84 85 /** 86 * Load a JSON key from a well known path. 87 * 88 * The well known path is OS dependent: 89 * 90 * * windows: %APPDATA%/gcloud/application_default_credentials.json 91 * * others: $HOME/.config/gcloud/application_default_credentials.json 92 * 93 * If the file does not exist, this returns null. 94 * 95 * @return array<mixed>|null JSON key | null 96 */ 97 public static function fromWellKnownFile() 98 { 99 $rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME'; 100 $path = [getenv($rootEnv)]; 101 if (!self::isOnWindows()) { 102 $path[] = self::NON_WINDOWS_WELL_KNOWN_PATH_BASE; 103 } 104 $path[] = self::WELL_KNOWN_PATH; 105 $path = implode(DIRECTORY_SEPARATOR, $path); 106 if (!file_exists($path)) { 107 return null; 108 } 109 $jsonKey = file_get_contents($path); 110 return json_decode((string) $jsonKey, true); 111 } 112 113 /** 114 * Create a new Credentials instance. 115 * 116 * @param string|string[] $scope the scope of the access request, expressed 117 * either as an Array or as a space-delimited String. 118 * @param array<mixed> $jsonKey the JSON credentials. 119 * @param string|string[] $defaultScope The default scope to use if no 120 * user-defined scopes exist, expressed either as an Array or as a 121 * space-delimited string. 122 * 123 * @return ServiceAccountCredentials|UserRefreshCredentials 124 */ 125 public static function makeCredentials( 126 $scope, 127 array $jsonKey, 128 $defaultScope = null 129 ) { 130 if (!array_key_exists('type', $jsonKey)) { 131 throw new \InvalidArgumentException('json key is missing the type field'); 132 } 133 134 if ($jsonKey['type'] == 'service_account') { 135 // Do not pass $defaultScope to ServiceAccountCredentials 136 return new ServiceAccountCredentials($scope, $jsonKey); 137 } 138 139 if ($jsonKey['type'] == 'authorized_user') { 140 $anyScope = $scope ?: $defaultScope; 141 return new UserRefreshCredentials($anyScope, $jsonKey); 142 } 143 144 throw new \InvalidArgumentException('invalid value in the type field'); 145 } 146 147 /** 148 * Create an authorized HTTP Client from an instance of FetchAuthTokenInterface. 149 * 150 * @param FetchAuthTokenInterface $fetcher is used to fetch the auth token 151 * @param array<mixed> $httpClientOptions (optional) Array of request options to apply. 152 * @param callable $httpHandler (optional) http client to fetch the token. 153 * @param callable $tokenCallback (optional) function to be called when a new token is fetched. 154 * @return \GuzzleHttp\Client 155 */ 156 public static function makeHttpClient( 157 FetchAuthTokenInterface $fetcher, 158 array $httpClientOptions = [], 159 callable $httpHandler = null, 160 callable $tokenCallback = null 161 ) { 162 $middleware = new Middleware\AuthTokenMiddleware( 163 $fetcher, 164 $httpHandler, 165 $tokenCallback 166 ); 167 $stack = \GuzzleHttp\HandlerStack::create(); 168 $stack->push($middleware); 169 170 return new \GuzzleHttp\Client([ 171 'handler' => $stack, 172 'auth' => 'google_auth', 173 ] + $httpClientOptions); 174 } 175 176 /** 177 * Create a new instance of InsecureCredentials. 178 * 179 * @return InsecureCredentials 180 */ 181 public static function makeInsecureCredentials() 182 { 183 return new InsecureCredentials(); 184 } 185 186 /** 187 * export a callback function which updates runtime metadata. 188 * 189 * @return callable updateMetadata function 190 * @deprecated 191 */ 192 public function getUpdateMetadataFunc() 193 { 194 return array($this, 'updateMetadata'); 195 } 196 197 /** 198 * Updates metadata with the authorization token. 199 * 200 * @param array<mixed> $metadata metadata hashmap 201 * @param string $authUri optional auth uri 202 * @param callable $httpHandler callback which delivers psr7 request 203 * @return array<mixed> updated metadata hashmap 204 */ 205 public function updateMetadata( 206 $metadata, 207 $authUri = null, 208 callable $httpHandler = null 209 ) { 210 if (isset($metadata[self::AUTH_METADATA_KEY])) { 211 // Auth metadata has already been set 212 return $metadata; 213 } 214 $result = $this->fetchAuthToken($httpHandler); 215 if (!isset($result['access_token'])) { 216 return $metadata; 217 } 218 $metadata_copy = $metadata; 219 $metadata_copy[self::AUTH_METADATA_KEY] = array('Bearer ' . $result['access_token']); 220 221 return $metadata_copy; 222 } 223 224 /** 225 * Gets a callable which returns the default device certification. 226 * 227 * @throws UnexpectedValueException 228 * @return callable|null 229 */ 230 public static function getDefaultClientCertSource() 231 { 232 if (!$clientCertSourceJson = self::loadDefaultClientCertSourceFile()) { 233 return null; 234 } 235 $clientCertSourceCmd = $clientCertSourceJson['cert_provider_command']; 236 237 return function () use ($clientCertSourceCmd) { 238 $cmd = array_map('escapeshellarg', $clientCertSourceCmd); 239 exec(implode(' ', $cmd), $output, $returnVar); 240 241 if (0 === $returnVar) { 242 return implode(PHP_EOL, $output); 243 } 244 throw new RuntimeException( 245 '"cert_provider_command" failed with a nonzero exit code' 246 ); 247 }; 248 } 249 250 /** 251 * Determines whether or not the default device certificate should be loaded. 252 * 253 * @return bool 254 */ 255 public static function shouldLoadClientCertSource() 256 { 257 return filter_var(getenv(self::MTLS_CERT_ENV_VAR), FILTER_VALIDATE_BOOLEAN); 258 } 259 260 /** 261 * @return array{cert_provider_command:string[]}|null 262 */ 263 private static function loadDefaultClientCertSourceFile() 264 { 265 $rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME'; 266 $path = sprintf('%s/%s', getenv($rootEnv), self::MTLS_WELL_KNOWN_PATH); 267 if (!file_exists($path)) { 268 return null; 269 } 270 $jsonKey = file_get_contents($path); 271 $clientCertSourceJson = json_decode((string) $jsonKey, true); 272 if (!$clientCertSourceJson) { 273 throw new UnexpectedValueException('Invalid client cert source JSON'); 274 } 275 if (!isset($clientCertSourceJson['cert_provider_command'])) { 276 throw new UnexpectedValueException( 277 'cert source requires "cert_provider_command"' 278 ); 279 } 280 if (!is_array($clientCertSourceJson['cert_provider_command'])) { 281 throw new UnexpectedValueException( 282 'cert source expects "cert_provider_command" to be an array' 283 ); 284 } 285 return $clientCertSourceJson; 286 } 287} 288