1<?php
2/*
3 * Copyright 2010 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 Psr\Cache\CacheItemPoolInterface;
21
22/**
23 * A class to implement caching for any object implementing
24 * FetchAuthTokenInterface
25 */
26class FetchAuthTokenCache implements
27    FetchAuthTokenInterface,
28    GetQuotaProjectInterface,
29    SignBlobInterface,
30    ProjectIdProviderInterface,
31    UpdateMetadataInterface
32{
33    use CacheTrait;
34
35    /**
36     * @var FetchAuthTokenInterface
37     */
38    private $fetcher;
39
40    /**
41     * @param FetchAuthTokenInterface $fetcher A credentials fetcher
42     * @param array<mixed> $cacheConfig Configuration for the cache
43     * @param CacheItemPoolInterface $cache
44     */
45    public function __construct(
46        FetchAuthTokenInterface $fetcher,
47        array $cacheConfig = null,
48        CacheItemPoolInterface $cache
49    ) {
50        $this->fetcher = $fetcher;
51        $this->cache = $cache;
52        $this->cacheConfig = array_merge([
53            'lifetime' => 1500,
54            'prefix' => '',
55        ], (array) $cacheConfig);
56    }
57
58    /**
59     * Implements FetchAuthTokenInterface#fetchAuthToken.
60     *
61     * Checks the cache for a valid auth token and fetches the auth tokens
62     * from the supplied fetcher.
63     *
64     * @param callable $httpHandler callback which delivers psr7 request
65     * @return array<mixed> the response
66     * @throws \Exception
67     */
68    public function fetchAuthToken(callable $httpHandler = null)
69    {
70        if ($cached = $this->fetchAuthTokenFromCache()) {
71            return $cached;
72        }
73
74        $auth_token = $this->fetcher->fetchAuthToken($httpHandler);
75
76        $this->saveAuthTokenInCache($auth_token);
77
78        return $auth_token;
79    }
80
81    /**
82     * @return string
83     */
84    public function getCacheKey()
85    {
86        return $this->getFullCacheKey($this->fetcher->getCacheKey());
87    }
88
89    /**
90     * @return array<mixed>|null
91     */
92    public function getLastReceivedToken()
93    {
94        return $this->fetcher->getLastReceivedToken();
95    }
96
97    /**
98     * Get the client name from the fetcher.
99     *
100     * @param callable $httpHandler An HTTP handler to deliver PSR7 requests.
101     * @return string
102     */
103    public function getClientName(callable $httpHandler = null)
104    {
105        if (!$this->fetcher instanceof SignBlobInterface) {
106            throw new \RuntimeException(
107                'Credentials fetcher does not implement ' .
108                'Google\Auth\SignBlobInterface'
109            );
110        }
111
112        return $this->fetcher->getClientName($httpHandler);
113    }
114
115    /**
116     * Sign a blob using the fetcher.
117     *
118     * @param string $stringToSign The string to sign.
119     * @param bool $forceOpenSsl Require use of OpenSSL for local signing. Does
120     *        not apply to signing done using external services. **Defaults to**
121     *        `false`.
122     * @return string The resulting signature.
123     * @throws \RuntimeException If the fetcher does not implement
124     *     `Google\Auth\SignBlobInterface`.
125     */
126    public function signBlob($stringToSign, $forceOpenSsl = false)
127    {
128        if (!$this->fetcher instanceof SignBlobInterface) {
129            throw new \RuntimeException(
130                'Credentials fetcher does not implement ' .
131                'Google\Auth\SignBlobInterface'
132            );
133        }
134
135        // Pass the access token from cache to GCECredentials for signing a blob.
136        // This saves a call to the metadata server when a cached token exists.
137        if ($this->fetcher instanceof Credentials\GCECredentials) {
138            $cached = $this->fetchAuthTokenFromCache();
139            $accessToken = isset($cached['access_token']) ? $cached['access_token'] : null;
140            return $this->fetcher->signBlob($stringToSign, $forceOpenSsl, $accessToken);
141        }
142
143        return $this->fetcher->signBlob($stringToSign, $forceOpenSsl);
144    }
145
146    /**
147     * Get the quota project used for this API request from the credentials
148     * fetcher.
149     *
150     * @return string|null
151     */
152    public function getQuotaProject()
153    {
154        if ($this->fetcher instanceof GetQuotaProjectInterface) {
155            return $this->fetcher->getQuotaProject();
156        }
157
158        return null;
159    }
160
161    /*
162     * Get the Project ID from the fetcher.
163     *
164     * @param callable $httpHandler Callback which delivers psr7 request
165     * @return string|null
166     * @throws \RuntimeException If the fetcher does not implement
167     *     `Google\Auth\ProvidesProjectIdInterface`.
168     */
169    public function getProjectId(callable $httpHandler = null)
170    {
171        if (!$this->fetcher instanceof ProjectIdProviderInterface) {
172            throw new \RuntimeException(
173                'Credentials fetcher does not implement ' .
174                'Google\Auth\ProvidesProjectIdInterface'
175            );
176        }
177
178        return $this->fetcher->getProjectId($httpHandler);
179    }
180
181    /**
182     * Updates metadata with the authorization token.
183     *
184     * @param array<mixed> $metadata metadata hashmap
185     * @param string $authUri optional auth uri
186     * @param callable $httpHandler callback which delivers psr7 request
187     * @return array<mixed> updated metadata hashmap
188     * @throws \RuntimeException If the fetcher does not implement
189     *     `Google\Auth\UpdateMetadataInterface`.
190     */
191    public function updateMetadata(
192        $metadata,
193        $authUri = null,
194        callable $httpHandler = null
195    ) {
196        if (!$this->fetcher instanceof UpdateMetadataInterface) {
197            throw new \RuntimeException(
198                'Credentials fetcher does not implement ' .
199                'Google\Auth\UpdateMetadataInterface'
200            );
201        }
202
203        $cached = $this->fetchAuthTokenFromCache($authUri);
204        if ($cached) {
205            // Set the access token in the `Authorization` metadata header so
206            // the downstream call to updateMetadata know they don't need to
207            // fetch another token.
208            if (isset($cached['access_token'])) {
209                $metadata[self::AUTH_METADATA_KEY] = [
210                    'Bearer ' . $cached['access_token']
211                ];
212            }
213        }
214
215        $newMetadata = $this->fetcher->updateMetadata(
216            $metadata,
217            $authUri,
218            $httpHandler
219        );
220
221        if (!$cached && $token = $this->fetcher->getLastReceivedToken()) {
222            $this->saveAuthTokenInCache($token, $authUri);
223        }
224
225        return $newMetadata;
226    }
227
228    /**
229     * @param string|null $authUri
230     * @return array<mixed>|null
231     */
232    private function fetchAuthTokenFromCache($authUri = null)
233    {
234        // Use the cached value if its available.
235        //
236        // TODO: correct caching; update the call to setCachedValue to set the expiry
237        // to the value returned with the auth token.
238        //
239        // TODO: correct caching; enable the cache to be cleared.
240
241        // if $authUri is set, use it as the cache key
242        $cacheKey = $authUri
243            ? $this->getFullCacheKey($authUri)
244            : $this->fetcher->getCacheKey();
245
246        $cached = $this->getCachedValue($cacheKey);
247        if (is_array($cached)) {
248            if (empty($cached['expires_at'])) {
249                // If there is no expiration data, assume token is not expired.
250                // (for JwtAccess and ID tokens)
251                return $cached;
252            }
253            if (time() < $cached['expires_at']) {
254                // access token is not expired
255                return $cached;
256            }
257        }
258
259        return null;
260    }
261
262    /**
263     * @param array<mixed> $authToken
264     * @param string|null  $authUri
265     * @return void
266     */
267    private function saveAuthTokenInCache($authToken, $authUri = null)
268    {
269        if (isset($authToken['access_token']) ||
270            isset($authToken['id_token'])) {
271            // if $authUri is set, use it as the cache key
272            $cacheKey = $authUri
273                ? $this->getFullCacheKey($authUri)
274                : $this->fetcher->getCacheKey();
275
276            $this->setCachedValue($cacheKey, $authToken);
277        }
278    }
279}
280