← All changes
|
includes/sdk/google/google/auth/src/Credentials/ServiceAccountCredentials.php
+79
-31
1.0.3
→
1.4.2
View file →
| @@ -14,16 +14,18 @@ | ||
| 14 | 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | 15 | * See the License for the specific language governing permissions and |
| 16 | 16 | * limitations under the License. |
| 17 | 17 | */ |
| 18 | -namespace Dudlewebs\WPMCS\Google\Auth\Credentials; | |
| 18 | +namespace Dudlewebs\WPMCS\GCP\Google\Auth\Credentials; | |
| 19 | 19 | |
| 20 | -use Dudlewebs\WPMCS\Google\Auth\CredentialsLoader; | |
| 21 | -use Dudlewebs\WPMCS\Google\Auth\GetQuotaProjectInterface; | |
| 22 | -use Dudlewebs\WPMCS\Google\Auth\OAuth2; | |
| 23 | -use Dudlewebs\WPMCS\Google\Auth\ProjectIdProviderInterface; | |
| 24 | -use Dudlewebs\WPMCS\Google\Auth\ServiceAccountSignerTrait; | |
| 25 | -use Dudlewebs\WPMCS\Google\Auth\SignBlobInterface; | |
| 20 | +use Dudlewebs\WPMCS\GCP\Firebase\JWT\JWT; | |
| 21 | +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialsLoader; | |
| 22 | +use Dudlewebs\WPMCS\GCP\Google\Auth\GetQuotaProjectInterface; | |
| 23 | +use Dudlewebs\WPMCS\GCP\Google\Auth\Iam; | |
| 24 | +use Dudlewebs\WPMCS\GCP\Google\Auth\OAuth2; | |
| 25 | +use Dudlewebs\WPMCS\GCP\Google\Auth\ProjectIdProviderInterface; | |
| 26 | +use Dudlewebs\WPMCS\GCP\Google\Auth\ServiceAccountSignerTrait; | |
| 27 | +use Dudlewebs\WPMCS\GCP\Google\Auth\SignBlobInterface; | |
| 26 | 28 | use InvalidArgumentException; |
| 27 | 29 | /** |
| 28 | 30 | * ServiceAccountCredentials supports authorization using a Google service |
| 29 | 31 | * account. |
| @@ -60,8 +62,15 @@ | ||
| 60 | 62 | class ServiceAccountCredentials extends CredentialsLoader implements GetQuotaProjectInterface, SignBlobInterface, ProjectIdProviderInterface |
| 61 | 63 | { |
| 62 | 64 | use ServiceAccountSignerTrait; |
| 63 | 65 | /** |
| 66 | + * Used in observability metric headers | |
| 67 | + * | |
| 68 | + * @var string | |
| 69 | + */ | |
| 70 | + private const CRED_TYPE = 'sa'; | |
| 71 | + private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam'; | |
| 72 | + /** | |
| 64 | 73 | * The OAuth2 instance used to conduct authorization. |
| 65 | 74 | * |
| 66 | 75 | * @var OAuth2 |
| 67 | 76 | */ |
| @@ -92,8 +101,13 @@ | ||
| 92 | 101 | * @var string |
| 93 | 102 | */ |
| 94 | 103 | private string $universeDomain; |
| 95 | 104 | /** |
| 105 | + * Whether this is an ID token request or an access token request. Used when | |
| 106 | + * building the metric header. | |
| 107 | + */ | |
| 108 | + private bool $isIdTokenRequest = \false; | |
| 109 | + /** | |
| 96 | 110 | * Create a new ServiceAccountCredentials. |
| 97 | 111 | * |
| 98 | 112 | * @param string|string[]|null $scope the scope of the access request, expressed |
| 99 | 113 | * either as an Array or as a space-delimited String. |
| @@ -104,24 +118,24 @@ | ||
| 104 | 118 | * @param string $targetAudience The audience for the ID token. |
| 105 | 119 | */ |
| 106 | 120 | public function __construct($scope, $jsonKey, $sub = null, $targetAudience = null) |
| 107 | 121 | { |
| 108 | - if (is_string($jsonKey)) { | |
| 109 | - if (!file_exists($jsonKey)) { | |
| 122 | + if (\is_string($jsonKey)) { | |
| 123 | + if (!\file_exists($jsonKey)) { | |
| 110 | 124 | throw new \InvalidArgumentException('file does not exist'); |
| 111 | 125 | } |
| 112 | - $jsonKeyStream = file_get_contents($jsonKey); | |
| 113 | - if (!$jsonKey = json_decode((string) $jsonKeyStream, \true)) { | |
| 126 | + $jsonKeyStream = \file_get_contents($jsonKey); | |
| 127 | + if (!($jsonKey = \json_decode((string) $jsonKeyStream, \true))) { | |
| 114 | 128 | throw new \LogicException('invalid json for auth config'); |
| 115 | 129 | } |
| 116 | 130 | } |
| 117 | - if (!array_key_exists('client_email', $jsonKey)) { | |
| 131 | + if (!\array_key_exists('client_email', $jsonKey)) { | |
| 118 | 132 | throw new \InvalidArgumentException('json key is missing the client_email field'); |
| 119 | 133 | } |
| 120 | - if (!array_key_exists('private_key', $jsonKey)) { | |
| 134 | + if (!\array_key_exists('private_key', $jsonKey)) { | |
| 121 | 135 | throw new \InvalidArgumentException('json key is missing the private_key field'); |
| 122 | 136 | } |
| 123 | - if (array_key_exists('quota_project_id', $jsonKey)) { | |
| 137 | + if (\array_key_exists('quota_project_id', $jsonKey)) { | |
| 124 | 138 | $this->quotaProject = (string) $jsonKey['quota_project_id']; |
| 125 | 139 | } |
| 126 | 140 | if ($scope && $targetAudience) { |
| 127 | 141 | throw new InvalidArgumentException('Scope and targetAudience cannot both be supplied'); |
| @@ -128,10 +142,11 @@ | ||
| 128 | 142 | } |
| 129 | 143 | $additionalClaims = []; |
| 130 | 144 | if ($targetAudience) { |
| 131 | 145 | $additionalClaims = ['target_audience' => $targetAudience]; |
| 146 | + $this->isIdTokenRequest = \true; | |
| 132 | 147 | } |
| 133 | - $this->auth = new OAuth2(['audience' => self::TOKEN_CREDENTIAL_URI, 'issuer' => $jsonKey['client_email'], 'scope' => $scope, 'signingAlgorithm' => 'RS256', 'signingKey' => $jsonKey['private_key'], 'sub' => $sub, 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 'additionalClaims' => $additionalClaims]); | |
| 148 | + $this->auth = new OAuth2(['audience' => self::TOKEN_CREDENTIAL_URI, 'issuer' => $jsonKey['client_email'], 'scope' => $scope, 'signingAlgorithm' => 'RS256', 'signingKey' => $jsonKey['private_key'], 'signingKeyId' => $jsonKey['private_key_id'] ?? null, 'sub' => $sub, 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 'additionalClaims' => $additionalClaims]); | |
| 134 | 149 | $this->projectId = $jsonKey['project_id'] ?? null; |
| 135 | 150 | $this->universeDomain = $jsonKey['universe_domain'] ?? self::DEFAULT_UNIVERSE_DOMAIN; |
| 136 | 151 | } |
| 137 | 152 | /** |
| @@ -147,9 +162,11 @@ | ||
| 147 | 162 | { |
| 148 | 163 | $this->useJwtAccessWithScope = \true; |
| 149 | 164 | } |
| 150 | 165 | /** |
| 151 | - * @param callable $httpHandler | |
| 166 | + * @param callable|null $httpHandler | |
| 167 | + * @param array<mixed> $headers [optional] Headers to be inserted | |
| 168 | + * into the token endpoint request present. | |
| 152 | 169 | * |
| 153 | 170 | * @return array<mixed> { |
| 154 | 171 | * A set of auth related metadata, containing the following |
| 155 | 172 | * |
| @@ -157,9 +174,9 @@ | ||
| 157 | 174 | * @type int $expires_in |
| 158 | 175 | * @type string $token_type |
| 159 | 176 | * } |
| 160 | 177 | */ |
| 161 | - public function fetchAuthToken(callable $httpHandler = null) | |
| 178 | + public function fetchAuthToken(?callable $httpHandler = null, array $headers = []) | |
| 162 | 179 | { |
| 163 | 180 | if ($this->useSelfSignedJwt()) { |
| 164 | 181 | $jwtCreds = $this->createJwtAccessCredentials(); |
| 165 | 182 | $accessToken = $jwtCreds->fetchAuthToken($httpHandler); |
| @@ -168,18 +185,34 @@ | ||
| 168 | 185 | $this->lastReceivedJwtAccessToken = $lastReceivedToken; |
| 169 | 186 | } |
| 170 | 187 | return $accessToken; |
| 171 | 188 | } |
| 172 | - return $this->auth->fetchAuthToken($httpHandler); | |
| 189 | + if ($this->isIdTokenRequest && $this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) { | |
| 190 | + $now = \time(); | |
| 191 | + $jwt = Jwt::encode(['iss' => $this->auth->getIssuer(), 'sub' => $this->auth->getIssuer(), 'scope' => self::IAM_SCOPE, 'exp' => $now + $this->auth->getExpiry(), 'iat' => $now - OAuth2::DEFAULT_SKEW_SECONDS], $this->auth->getSigningKey(), $this->auth->getSigningAlgorithm(), $this->auth->getSigningKeyId()); | |
| 192 | + // We create a new instance of Iam each time because the `$httpHandler` might change. | |
| 193 | + $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken($this->auth->getIssuer(), $this->auth->getAdditionalClaims()['target_audience'], $jwt, $this->applyTokenEndpointMetrics($headers, 'it')); | |
| 194 | + return ['id_token' => $idToken]; | |
| 195 | + } | |
| 196 | + return $this->auth->fetchAuthToken($httpHandler, $this->applyTokenEndpointMetrics($headers, $this->isIdTokenRequest ? 'it' : 'at')); | |
| 173 | 197 | } |
| 174 | 198 | /** |
| 199 | + * Return the Cache Key for the credentials. | |
| 200 | + * For the cache key format is one of the following: | |
| 201 | + * ClientEmail.Scope[.Sub] | |
| 202 | + * ClientEmail.Audience[.Sub] | |
| 203 | + * | |
| 175 | 204 | * @return string |
| 176 | 205 | */ |
| 177 | 206 | public function getCacheKey() |
| 178 | 207 | { |
| 179 | - $key = $this->auth->getIssuer() . ':' . $this->auth->getCacheKey(); | |
| 208 | + $scopeOrAudience = $this->auth->getScope(); | |
| 209 | + if (!$scopeOrAudience) { | |
| 210 | + $scopeOrAudience = $this->auth->getAudience(); | |
| 211 | + } | |
| 212 | + $key = $this->auth->getIssuer() . '.' . $scopeOrAudience; | |
| 180 | 213 | if ($sub = $this->auth->getSub()) { |
| 181 | - $key .= ':' . $sub; | |
| 214 | + $key .= '.' . $sub; | |
| 182 | 215 | } |
| 183 | 216 | return $key; |
| 184 | 217 | } |
| 185 | 218 | /** |
| @@ -195,12 +228,12 @@ | ||
| 195 | 228 | * Get the project ID from the service account keyfile. |
| 196 | 229 | * |
| 197 | 230 | * Returns null if the project ID does not exist in the keyfile. |
| 198 | 231 | * |
| 199 | - * @param callable $httpHandler Not used by this credentials type. | |
| 232 | + * @param callable|null $httpHandler Not used by this credentials type. | |
| 200 | 233 | * @return string|null |
| 201 | 234 | */ |
| 202 | - public function getProjectId(callable $httpHandler = null) | |
| 235 | + public function getProjectId(?callable $httpHandler = null) | |
| 203 | 236 | { |
| 204 | 237 | return $this->projectId; |
| 205 | 238 | } |
| 206 | 239 | /** |
| @@ -207,12 +240,12 @@ | ||
| 207 | 240 | * Updates metadata with the authorization token. |
| 208 | 241 | * |
| 209 | 242 | * @param array<mixed> $metadata metadata hashmap |
| 210 | 243 | * @param string $authUri optional auth uri |
| 211 | - * @param callable $httpHandler callback which delivers psr7 request | |
| 244 | + * @param callable|null $httpHandler callback which delivers psr7 request | |
| 212 | 245 | * @return array<mixed> updated metadata hashmap |
| 213 | 246 | */ |
| 214 | - public function updateMetadata($metadata, $authUri = null, callable $httpHandler = null) | |
| 247 | + public function updateMetadata($metadata, $authUri = null, ?callable $httpHandler = null) | |
| 215 | 248 | { |
| 216 | 249 | // scope exists. use oauth implementation |
| 217 | 250 | if (!$this->useSelfSignedJwt()) { |
| 218 | 251 | return parent::updateMetadata($metadata, $authUri, $httpHandler); |
| @@ -255,16 +288,27 @@ | ||
| 255 | 288 | * Get the client name from the keyfile. |
| 256 | 289 | * |
| 257 | 290 | * In this case, it returns the keyfile's client_email key. |
| 258 | 291 | * |
| 259 | - * @param callable $httpHandler Not used by this credentials type. | |
| 292 | + * @param callable|null $httpHandler Not used by this credentials type. | |
| 260 | 293 | * @return string |
| 261 | 294 | */ |
| 262 | - public function getClientName(callable $httpHandler = null) | |
| 295 | + public function getClientName(?callable $httpHandler = null) | |
| 263 | 296 | { |
| 264 | 297 | return $this->auth->getIssuer(); |
| 265 | 298 | } |
| 266 | 299 | /** |
| 300 | + * Get the private key from the keyfile. | |
| 301 | + * | |
| 302 | + * In this case, it returns the keyfile's private_key key, needed for JWT signing. | |
| 303 | + * | |
| 304 | + * @return string | |
| 305 | + */ | |
| 306 | + public function getPrivateKey() | |
| 307 | + { | |
| 308 | + return $this->auth->getSigningKey(); | |
| 309 | + } | |
| 310 | + /** | |
| 267 | 311 | * Get the quota project used for this API request |
| 268 | 312 | * |
| 269 | 313 | * @return string|null |
| 270 | 314 | */ |
| @@ -276,12 +320,16 @@ | ||
| 276 | 320 | * Get the universe domain configured in the JSON credential. |
| 277 | 321 | * |
| 278 | 322 | * @return string |
| 279 | 323 | */ |
| 280 | - public function getUniverseDomain(): string | |
| 324 | + public function getUniverseDomain() : string | |
| 281 | 325 | { |
| 282 | 326 | return $this->universeDomain; |
| 283 | 327 | } |
| 328 | + protected function getCredType() : string | |
| 329 | + { | |
| 330 | + return self::CRED_TYPE; | |
| 331 | + } | |
| 284 | 332 | /** |
| 285 | 333 | * @return bool |
| 286 | 334 | */ |
| 287 | 335 | private function useSelfSignedJwt() |
| @@ -290,14 +338,14 @@ | ||
| 290 | 338 | // with self-signed JWTs |
| 291 | 339 | if (null !== $this->auth->getSub()) { |
| 292 | 340 | // If we are outside the GDU, we can't use domain-wide delegation |
| 293 | 341 | if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) { |
| 294 | - throw new \LogicException(sprintf('Service Account subject is configured for the credential. Domain-wide ' . 'delegation is not supported in universes other than %s.', self::DEFAULT_UNIVERSE_DOMAIN)); | |
| 342 | + throw new \LogicException(\sprintf('Service Account subject is configured for the credential. Domain-wide ' . 'delegation is not supported in universes other than %s.', self::DEFAULT_UNIVERSE_DOMAIN)); | |
| 295 | 343 | } |
| 296 | 344 | return \false; |
| 297 | 345 | } |
| 298 | - // If claims are set, this call is for "id_tokens" | |
| 299 | - if ($this->auth->getAdditionalClaims()) { | |
| 346 | + // Do not use self-signed JWT for ID tokens | |
| 347 | + if ($this->isIdTokenRequest) { | |
| 300 | 348 | return \false; |
| 301 | 349 | } |
| 302 | 350 | // When true, ServiceAccountCredentials will always use JwtAccess for access tokens |
| 303 | 351 | if ($this->useJwtAccessWithScope) { |
| @@ -306,7 +354,7 @@ | ||
| 306 | 354 | // If the universe domain is outside the GDU, use JwtAccess for access tokens |
| 307 | 355 | if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) { |
| 308 | 356 | return \true; |
| 309 | 357 | } |
| 310 | - return is_null($this->auth->getScope()); | |
| 358 | + return \is_null($this->auth->getScope()); | |
| 311 | 359 | } |
| 312 | 360 | } |