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
20/*
21 * The AppIdentityService class is automatically defined on App Engine,
22 * so including this dependency is not necessary, and will result in a
23 * PHP fatal error in the App Engine environment.
24 */
25use google\appengine\api\app_identity\AppIdentityService;
26use Google\Auth\CredentialsLoader;
27use Google\Auth\ProjectIdProviderInterface;
28use Google\Auth\SignBlobInterface;
29
30/**
31 * @deprecated
32 *
33 * AppIdentityCredentials supports authorization on Google App Engine.
34 *
35 * It can be used to authorize requests using the AuthTokenMiddleware or
36 * AuthTokenSubscriber, but will only succeed if being run on App Engine:
37 *
38 * Example:
39 * ```
40 * use Google\Auth\Credentials\AppIdentityCredentials;
41 * use Google\Auth\Middleware\AuthTokenMiddleware;
42 * use GuzzleHttp\Client;
43 * use GuzzleHttp\HandlerStack;
44 *
45 * $gae = new AppIdentityCredentials('https://www.googleapis.com/auth/books');
46 * $middleware = new AuthTokenMiddleware($gae);
47 * $stack = HandlerStack::create();
48 * $stack->push($middleware);
49 *
50 * $client = new Client([
51 *     'handler' => $stack,
52 *     'base_uri' => 'https://www.googleapis.com/books/v1',
53 *     'auth' => 'google_auth'
54 * ]);
55 *
56 * $res = $client->get('volumes?q=Henry+David+Thoreau&country=US');
57 * ```
58 */
59class AppIdentityCredentials extends CredentialsLoader implements
60    SignBlobInterface,
61    ProjectIdProviderInterface
62{
63    /**
64     * Result of fetchAuthToken.
65     *
66     * @var array<mixed>
67     */
68    protected $lastReceivedToken;
69
70    /**
71     * Array of OAuth2 scopes to be requested.
72     *
73     * @var string[]
74     */
75    private $scope;
76
77    /**
78     * @var string
79     */
80    private $clientName;
81
82    /**
83     * @param string|string[] $scope One or more scopes.
84     */
85    public function __construct($scope = [])
86    {
87        $this->scope = is_array($scope) ? $scope : explode(' ', (string) $scope);
88    }
89
90    /**
91     * Determines if this an App Engine instance, by accessing the
92     * SERVER_SOFTWARE environment variable (prod) or the APPENGINE_RUNTIME
93     * environment variable (dev).
94     *
95     * @return bool true if this an App Engine Instance, false otherwise
96     */
97    public static function onAppEngine()
98    {
99        $appEngineProduction = isset($_SERVER['SERVER_SOFTWARE']) &&
100            0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine');
101        if ($appEngineProduction) {
102            return true;
103        }
104        $appEngineDevAppServer = isset($_SERVER['APPENGINE_RUNTIME']) &&
105            $_SERVER['APPENGINE_RUNTIME'] == 'php';
106        if ($appEngineDevAppServer) {
107            return true;
108        }
109        return false;
110    }
111
112    /**
113     * Implements FetchAuthTokenInterface#fetchAuthToken.
114     *
115     * Fetches the auth tokens using the AppIdentityService if available.
116     * As the AppIdentityService uses protobufs to fetch the access token,
117     * the GuzzleHttp\ClientInterface instance passed in will not be used.
118     *
119     * @param callable $httpHandler callback which delivers psr7 request
120     * @return array<mixed> {
121     *     A set of auth related metadata, containing the following
122     *
123     *     @type string $access_token
124     *     @type string $expiration_time
125     * }
126     */
127    public function fetchAuthToken(callable $httpHandler = null)
128    {
129        try {
130            $this->checkAppEngineContext();
131        } catch (\Exception $e) {
132            return [];
133        }
134
135        /** @phpstan-ignore-next-line */
136        $token = AppIdentityService::getAccessToken($this->scope);
137        $this->lastReceivedToken = $token;
138
139        return $token;
140    }
141
142    /**
143     * Sign a string using AppIdentityService.
144     *
145     * @param string $stringToSign The string to sign.
146     * @param bool $forceOpenSsl [optional] Does not apply to this credentials
147     *        type.
148     * @return string The signature, base64-encoded.
149     * @throws \Exception If AppEngine SDK or mock is not available.
150     */
151    public function signBlob($stringToSign, $forceOpenSsl = false)
152    {
153        $this->checkAppEngineContext();
154
155        /** @phpstan-ignore-next-line */
156        return base64_encode(AppIdentityService::signForApp($stringToSign)['signature']);
157    }
158
159    /**
160     * Get the project ID from AppIdentityService.
161     *
162     * Returns null if AppIdentityService is unavailable.
163     *
164     * @param callable $httpHandler Not used by this type.
165     * @return string|null
166     */
167    public function getProjectId(callable $httpHandler = null)
168    {
169        try {
170            $this->checkAppEngineContext();
171        } catch (\Exception $e) {
172            return null;
173        }
174
175        /** @phpstan-ignore-next-line */
176        return AppIdentityService::getApplicationId();
177    }
178
179    /**
180     * Get the client name from AppIdentityService.
181     *
182     * Subsequent calls to this method will return a cached value.
183     *
184     * @param callable $httpHandler Not used in this implementation.
185     * @return string
186     * @throws \Exception If AppEngine SDK or mock is not available.
187     */
188    public function getClientName(callable $httpHandler = null)
189    {
190        $this->checkAppEngineContext();
191
192        if (!$this->clientName) {
193            /** @phpstan-ignore-next-line */
194            $this->clientName = AppIdentityService::getServiceAccountName();
195        }
196
197        return $this->clientName;
198    }
199
200    /**
201     * @return array{access_token:string,expires_at:int}|null
202     */
203    public function getLastReceivedToken()
204    {
205        if ($this->lastReceivedToken) {
206            return [
207                'access_token' => $this->lastReceivedToken['access_token'],
208                'expires_at' => $this->lastReceivedToken['expiration_time'],
209            ];
210        }
211
212        return null;
213    }
214
215    /**
216     * Caching is handled by the underlying AppIdentityService, return empty string
217     * to prevent caching.
218     *
219     * @return string
220     */
221    public function getCacheKey()
222    {
223        return '';
224    }
225
226    /**
227     * @return void
228     */
229    private function checkAppEngineContext()
230    {
231        if (!self::onAppEngine() || !class_exists('google\appengine\api\app_identity\AppIdentityService')) {
232            throw new \Exception(
233                'This class must be run in App Engine, or you must include the AppIdentityService '
234                . 'mock class defined in tests/mocks/AppIdentityService.php'
235            );
236        }
237    }
238}
239