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 DomainException;
21use Google\Auth\Credentials\AppIdentityCredentials;
22use Google\Auth\Credentials\GCECredentials;
23use Google\Auth\Credentials\ServiceAccountCredentials;
24use Google\Auth\HttpHandler\HttpClientCache;
25use Google\Auth\HttpHandler\HttpHandlerFactory;
26use Google\Auth\Middleware\AuthTokenMiddleware;
27use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
28use Google\Auth\Subscriber\AuthTokenSubscriber;
29use GuzzleHttp\Client;
30use InvalidArgumentException;
31use Psr\Cache\CacheItemPoolInterface;
32
33/**
34 * ApplicationDefaultCredentials obtains the default credentials for
35 * authorizing a request to a Google service.
36 *
37 * Application Default Credentials are described here:
38 * https://developers.google.com/accounts/docs/application-default-credentials
39 *
40 * This class implements the search for the application default credentials as
41 * described in the link.
42 *
43 * It provides three factory methods:
44 * - #get returns the computed credentials object
45 * - #getSubscriber returns an AuthTokenSubscriber built from the credentials object
46 * - #getMiddleware returns an AuthTokenMiddleware built from the credentials object
47 *
48 * This allows it to be used as follows with GuzzleHttp\Client:
49 *
50 * ```
51 * use Google\Auth\ApplicationDefaultCredentials;
52 * use GuzzleHttp\Client;
53 * use GuzzleHttp\HandlerStack;
54 *
55 * $middleware = ApplicationDefaultCredentials::getMiddleware(
56 *     'https://www.googleapis.com/auth/taskqueue'
57 * );
58 * $stack = HandlerStack::create();
59 * $stack->push($middleware);
60 *
61 * $client = new Client([
62 *     'handler' => $stack,
63 *     'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
64 *     'auth' => 'google_auth' // authorize all requests
65 * ]);
66 *
67 * $res = $client->get('myproject/taskqueues/myqueue');
68 * ```
69 */
70class ApplicationDefaultCredentials
71{
72    /**
73     * @deprecated
74     *
75     * Obtains an AuthTokenSubscriber that uses the default FetchAuthTokenInterface
76     * implementation to use in this environment.
77     *
78     * If supplied, $scope is used to in creating the credentials instance if
79     * this does not fallback to the compute engine defaults.
80     *
81     * @param string|string[] $scope the scope of the access request, expressed
82     *        either as an Array or as a space-delimited String.
83     * @param callable $httpHandler callback which delivers psr7 request
84     * @param array<mixed> $cacheConfig configuration for the cache when it's present
85     * @param CacheItemPoolInterface $cache A cache implementation, may be
86     *        provided if you have one already available for use.
87     * @return AuthTokenSubscriber
88     * @throws DomainException if no implementation can be obtained.
89     */
90    public static function getSubscriber(// @phpstan-ignore-line
91        $scope = null,
92        callable $httpHandler = null,
93        array $cacheConfig = null,
94        CacheItemPoolInterface $cache = null
95    ) {
96        $creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache);
97
98        /** @phpstan-ignore-next-line */
99        return new AuthTokenSubscriber($creds, $httpHandler);
100    }
101
102    /**
103     * Obtains an AuthTokenMiddleware that uses the default FetchAuthTokenInterface
104     * implementation to use in this environment.
105     *
106     * If supplied, $scope is used to in creating the credentials instance if
107     * this does not fallback to the compute engine defaults.
108     *
109     * @param string|string[] $scope the scope of the access request, expressed
110     *        either as an Array or as a space-delimited String.
111     * @param callable $httpHandler callback which delivers psr7 request
112     * @param array<mixed> $cacheConfig configuration for the cache when it's present
113     * @param CacheItemPoolInterface $cache A cache implementation, may be
114     *        provided if you have one already available for use.
115     * @param string $quotaProject specifies a project to bill for access
116     *   charges associated with the request.
117     * @return AuthTokenMiddleware
118     * @throws DomainException if no implementation can be obtained.
119     */
120    public static function getMiddleware(
121        $scope = null,
122        callable $httpHandler = null,
123        array $cacheConfig = null,
124        CacheItemPoolInterface $cache = null,
125        $quotaProject = null
126    ) {
127        $creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache, $quotaProject);
128
129        return new AuthTokenMiddleware($creds, $httpHandler);
130    }
131
132    /**
133     * Obtains the default FetchAuthTokenInterface implementation to use
134     * in this environment.
135     *
136     * @param string|string[] $scope the scope of the access request, expressed
137     *        either as an Array or as a space-delimited String.
138     * @param callable $httpHandler callback which delivers psr7 request
139     * @param array<mixed> $cacheConfig configuration for the cache when it's present
140     * @param CacheItemPoolInterface $cache A cache implementation, may be
141     *        provided if you have one already available for use.
142     * @param string $quotaProject specifies a project to bill for access
143     *   charges associated with the request.
144     * @param string|string[] $defaultScope The default scope to use if no
145     *   user-defined scopes exist, expressed either as an Array or as a
146     *   space-delimited string.
147     *
148     * @return FetchAuthTokenInterface
149     * @throws DomainException if no implementation can be obtained.
150     */
151    public static function getCredentials(
152        $scope = null,
153        callable $httpHandler = null,
154        array $cacheConfig = null,
155        CacheItemPoolInterface $cache = null,
156        $quotaProject = null,
157        $defaultScope = null
158    ) {
159        $creds = null;
160        $jsonKey = CredentialsLoader::fromEnv()
161            ?: CredentialsLoader::fromWellKnownFile();
162        $anyScope = $scope ?: $defaultScope;
163
164        if (!$httpHandler) {
165            if (!($client = HttpClientCache::getHttpClient())) {
166                $client = new Client();
167                HttpClientCache::setHttpClient($client);
168            }
169
170            $httpHandler = HttpHandlerFactory::build($client);
171        }
172
173        if (!is_null($jsonKey)) {
174            if ($quotaProject) {
175                $jsonKey['quota_project_id'] = $quotaProject;
176            }
177            $creds = CredentialsLoader::makeCredentials(
178                $scope,
179                $jsonKey,
180                $defaultScope
181            );
182        } elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
183            $creds = new AppIdentityCredentials($anyScope);
184        } elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
185            $creds = new GCECredentials(null, $anyScope, null, $quotaProject);
186        }
187
188        if (is_null($creds)) {
189            throw new DomainException(self::notFound());
190        }
191        if (!is_null($cache)) {
192            $creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
193        }
194        return $creds;
195    }
196
197    /**
198     * Obtains an AuthTokenMiddleware which will fetch an ID token to use in the
199     * Authorization header. The middleware is configured with the default
200     * FetchAuthTokenInterface implementation to use in this environment.
201     *
202     * If supplied, $targetAudience is used to set the "aud" on the resulting
203     * ID token.
204     *
205     * @param string $targetAudience The audience for the ID token.
206     * @param callable $httpHandler callback which delivers psr7 request
207     * @param array<mixed> $cacheConfig configuration for the cache when it's present
208     * @param CacheItemPoolInterface $cache A cache implementation, may be
209     *        provided if you have one already available for use.
210     * @return AuthTokenMiddleware
211     * @throws DomainException if no implementation can be obtained.
212     */
213    public static function getIdTokenMiddleware(
214        $targetAudience,
215        callable $httpHandler = null,
216        array $cacheConfig = null,
217        CacheItemPoolInterface $cache = null
218    ) {
219        $creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
220
221        return new AuthTokenMiddleware($creds, $httpHandler);
222    }
223
224    /**
225     * Obtains an ProxyAuthTokenMiddleware which will fetch an ID token to use in the
226     * Authorization header. The middleware is configured with the default
227     * FetchAuthTokenInterface implementation to use in this environment.
228     *
229     * If supplied, $targetAudience is used to set the "aud" on the resulting
230     * ID token.
231     *
232     * @param string $targetAudience The audience for the ID token.
233     * @param callable $httpHandler callback which delivers psr7 request
234     * @param array<mixed> $cacheConfig configuration for the cache when it's present
235     * @param CacheItemPoolInterface $cache A cache implementation, may be
236     *        provided if you have one already available for use.
237     * @return ProxyAuthTokenMiddleware
238     * @throws DomainException if no implementation can be obtained.
239     */
240    public static function getProxyIdTokenMiddleware(
241        $targetAudience,
242        callable $httpHandler = null,
243        array $cacheConfig = null,
244        CacheItemPoolInterface $cache = null
245    ) {
246        $creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
247
248        return new ProxyAuthTokenMiddleware($creds, $httpHandler);
249    }
250
251    /**
252     * Obtains the default FetchAuthTokenInterface implementation to use
253     * in this environment, configured with a $targetAudience for fetching an ID
254     * token.
255     *
256     * @param string $targetAudience The audience for the ID token.
257     * @param callable $httpHandler callback which delivers psr7 request
258     * @param array<mixed> $cacheConfig configuration for the cache when it's present
259     * @param CacheItemPoolInterface $cache A cache implementation, may be
260     *        provided if you have one already available for use.
261     * @return FetchAuthTokenInterface
262     * @throws DomainException if no implementation can be obtained.
263     * @throws InvalidArgumentException if JSON "type" key is invalid
264     */
265    public static function getIdTokenCredentials(
266        $targetAudience,
267        callable $httpHandler = null,
268        array $cacheConfig = null,
269        CacheItemPoolInterface $cache = null
270    ) {
271        $creds = null;
272        $jsonKey = CredentialsLoader::fromEnv()
273            ?: CredentialsLoader::fromWellKnownFile();
274
275        if (!$httpHandler) {
276            if (!($client = HttpClientCache::getHttpClient())) {
277                $client = new Client();
278                HttpClientCache::setHttpClient($client);
279            }
280
281            $httpHandler = HttpHandlerFactory::build($client);
282        }
283
284        if (!is_null($jsonKey)) {
285            if (!array_key_exists('type', $jsonKey)) {
286                throw new \InvalidArgumentException('json key is missing the type field');
287            }
288
289            if ($jsonKey['type'] == 'authorized_user') {
290                throw new InvalidArgumentException('ID tokens are not supported for end user credentials');
291            }
292
293            if ($jsonKey['type'] != 'service_account') {
294                throw new InvalidArgumentException('invalid value in the type field');
295            }
296
297            $creds = new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience);
298        } elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
299            $creds = new GCECredentials(null, null, $targetAudience);
300        }
301
302        if (is_null($creds)) {
303            throw new DomainException(self::notFound());
304        }
305        if (!is_null($cache)) {
306            $creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
307        }
308        return $creds;
309    }
310
311    /**
312     * @return string
313     */
314    private static function notFound()
315    {
316        $msg = 'Could not load the default credentials. Browse to ';
317        $msg .= 'https://developers.google.com';
318        $msg .= '/accounts/docs/application-default-credentials';
319        $msg .= ' for more information';
320
321        return $msg;
322    }
323
324    /**
325     * @param callable $httpHandler
326     * @param array<mixed> $cacheConfig
327     * @param CacheItemPoolInterface $cache
328     * @return bool
329     */
330    private static function onGce(
331        callable $httpHandler = null,
332        array $cacheConfig = null,
333        CacheItemPoolInterface $cache = null
334    ) {
335        $gceCacheConfig = [];
336        foreach (['lifetime', 'prefix'] as $key) {
337            if (isset($cacheConfig['gce_' . $key])) {
338                $gceCacheConfig[$key] = $cacheConfig['gce_' . $key];
339            }
340        }
341
342        return (new GCECache($gceCacheConfig, $cache))->onGce($httpHandler);
343    }
344}
345