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