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