2121use Google \Auth \CacheTrait ;
2222use Google \Auth \CredentialsLoader ;
2323use Google \Auth \FetchAuthTokenInterface ;
24+ use Google \Auth \GetUniverseDomainInterface ;
2425use Google \Auth \HttpHandler \HttpClientCache ;
2526use Google \Auth \HttpHandler \HttpHandlerFactory ;
2627use Google \Auth \IamSignerTrait ;
2930use InvalidArgumentException ;
3031use LogicException ;
3132
32- class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface
33+ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
34+ SignBlobInterface,
35+ GetUniverseDomainInterface
3336{
3437 use CacheTrait;
3538 use IamSignerTrait;
3639
3740 private const CRED_TYPE = 'imp ' ;
41+ private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam ' ;
42+ private const ID_TOKEN_IMPERSONATION_URL =
43+ 'https://iamcredentials.UNIVERSE_DOMAIN/v1/projects/-/serviceAccounts/%s:generateIdToken ' ;
3844
3945 /**
4046 * @var string
@@ -71,10 +77,12 @@ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
7177 * @type int $lifetime The lifetime of the impersonated credentials
7278 * @type string[] $delegates The delegates to impersonate
7379 * }
80+ * @param string|null $targetAudience The audience to request an ID token.
7481 */
7582 public function __construct (
76- $ scope ,
77- $ jsonKey
83+ string |array |null $ scope ,
84+ string |array $ jsonKey ,
85+ private ?string $ targetAudience = null
7886 ) {
7987 if (is_string ($ jsonKey )) {
8088 if (!file_exists ($ jsonKey )) {
@@ -93,10 +101,23 @@ public function __construct(
93101 if (!array_key_exists ('source_credentials ' , $ jsonKey )) {
94102 throw new LogicException ('json key is missing the source_credentials field ' );
95103 }
104+ if ($ scope && $ targetAudience ) {
105+ throw new InvalidArgumentException (
106+ 'Scope and targetAudience cannot both be supplied '
107+ );
108+ }
96109 if (is_array ($ jsonKey ['source_credentials ' ])) {
97110 if (!array_key_exists ('type ' , $ jsonKey ['source_credentials ' ])) {
98111 throw new InvalidArgumentException ('json key source credentials are missing the type field ' );
99112 }
113+ if (
114+ $ targetAudience !== null
115+ && $ jsonKey ['source_credentials ' ]['type ' ] === 'service_account '
116+ ) {
117+ // Service account tokens MUST request a scope, and as this token is only used to impersonate
118+ // an ID token, the narrowest scope we can request is `iam`.
119+ $ scope = self ::IAM_SCOPE ;
120+ }
100121 $ jsonKey ['source_credentials ' ] = CredentialsLoader::makeCredentials ($ scope , $ jsonKey ['source_credentials ' ]);
101122 }
102123
@@ -171,28 +192,52 @@ public function fetchAuthToken(?callable $httpHandler = null)
171192 'Content-Type ' => 'application/json ' ,
172193 'Cache-Control ' => 'no-store ' ,
173194 'Authorization ' => sprintf ('Bearer %s ' , $ authToken ['access_token ' ] ?? $ authToken ['id_token ' ]),
174- ], 'at ' );
195+ ], $ this ->isIdTokenRequest () ? 'it ' : 'at ' );
196+
197+ $ body = match ($ this ->isIdTokenRequest ()) {
198+ true => [
199+ 'audience ' => $ this ->targetAudience ,
200+ 'includeEmail ' => true ,
201+ ],
202+ false => [
203+ 'scope ' => $ this ->targetScope ,
204+ 'delegates ' => $ this ->delegates ,
205+ 'lifetime ' => sprintf ('%ss ' , $ this ->lifetime ),
206+ ]
207+ };
175208
176- $ body = [
177- 'scope ' => $ this ->targetScope ,
178- 'delegates ' => $ this ->delegates ,
179- 'lifetime ' => sprintf ('%ss ' , $ this ->lifetime ),
180- ];
209+ $ url = $ this ->serviceAccountImpersonationUrl ;
210+ if ($ this ->isIdTokenRequest ()) {
211+ $ regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/ ' ;
212+ if (!preg_match ($ regex , $ url , $ matches )) {
213+ throw new InvalidArgumentException (
214+ 'Invalid service account impersonation URL - unable to parse service account email '
215+ );
216+ }
217+ $ url = str_replace (
218+ 'UNIVERSE_DOMAIN ' ,
219+ $ this ->getUniverseDomain (),
220+ sprintf (self ::ID_TOKEN_IMPERSONATION_URL , $ matches ['email ' ])
221+ );
222+ }
181223
182224 $ request = new Request (
183225 'POST ' ,
184- $ this -> serviceAccountImpersonationUrl ,
226+ $ url ,
185227 $ headers ,
186228 (string ) json_encode ($ body )
187229 );
188230
189231 $ response = $ httpHandler ($ request );
190232 $ body = json_decode ((string ) $ response ->getBody (), true );
191233
192- return [
193- 'access_token ' => $ body ['accessToken ' ],
194- 'expires_at ' => strtotime ($ body ['expireTime ' ]),
195- ];
234+ return match ($ this ->isIdTokenRequest ()) {
235+ true => ['id_token ' => $ body ['token ' ]],
236+ false => [
237+ 'access_token ' => $ body ['accessToken ' ],
238+ 'expires_at ' => strtotime ($ body ['expireTime ' ]),
239+ ]
240+ };
196241 }
197242
198243 /**
@@ -220,4 +265,16 @@ protected function getCredType(): string
220265 {
221266 return self ::CRED_TYPE ;
222267 }
268+
269+ private function isIdTokenRequest (): bool
270+ {
271+ return !is_null ($ this ->targetAudience );
272+ }
273+
274+ public function getUniverseDomain (): string
275+ {
276+ return $ this ->sourceCredentials instanceof GetUniverseDomainInterface
277+ ? $ this ->sourceCredentials ->getUniverseDomain ()
278+ : self ::DEFAULT_UNIVERSE_DOMAIN ;
279+ }
223280}
0 commit comments