Skip to content

Commit cea42a3

Browse files
ajs424Aalap Shastri
andauthored
Add adc support to php client lib (#1115)
* Add adc support to php client lib Change-Id: If39acfaed45509eb0301bb585b1e6751fc98092f * Updated ADC implementation for PHP client lib Change-Id: I7598112b72fb9f7596e1c719b76f6acc1e65646f * content cleanup for adc support Change-Id: I97d2790d46b21e57de59cc263d70133d9af3b7c6 * Add unit tests for ADC support Change-Id: I86448f9f8e44b206847bd3ce273c93807674d8c7 * Code cleanup Change-Id: I62e3ecea661b0cf40cd2eeac7cadb776744d8f07 * Refactor scopes Change-Id: Ibea3b010818553bcc22ab976db8f425215119e65 * refactor ADC implementation and OAuth2 scope handling Change-Id: I396d276d57b890e110840882cae74a7e6bd341ee * refactor ADC implementation and update test method Change-Id: I987a3723ccf5cc57e388c8e39d7a419fcef15fcd * Additional refactoring and remove changelog edits Change-Id: Ibdb4f9a450a366fb551a578346907dc0b5360283 * update import statement Change-Id: I5ea7df214fb295ddd92d3c8ad329d1737014560c --------- Co-authored-by: Aalap Shastri <aalapshastri@google.com>
1 parent 58e3f24 commit cea42a3

File tree

5 files changed

+269
-54
lines changed

5 files changed

+269
-54
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"google/gax": "^1.19.1",
88
"grpc/grpc": ">=1.36.0 <=1.57.0",
99
"google/protobuf": "^3.21.5 || >=4.26 <=4.30.0",
10-
"monolog/monolog": "^1.26 || ^2.0 || ^3.0"
10+
"monolog/monolog": "^1.26 || ^2.0 || ^3.0",
11+
"google/auth": "^1.30 || ^2.0"
1112
},
1213
"require-dev": {
1314
"phpunit/phpunit": "^9.5",

examples/Authentication/google_ads_php.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ developerToken = "INSERT_DEVELOPER_TOKEN_HERE"
2828
; Required OAuth2 credentials. Uncomment and fill in the values for the
2929
; appropriate flow based on your use case.
3030

31+
; Application Default Credentials (ADC)
32+
; If no explicit credentials (clientId/secret/refreshToken or jsonKeyFilePath)
33+
; are provided below, the client library will automatically attempt to use ADC
34+
; to find credentials based on the environment (e.g., GOOGLE_APPLICATION_CREDENTIALS
35+
; environment variable, 'gcloud auth application-default login', or compute engine service account).
36+
; Explicitly set credentials take precedence over ADC.
37+
; See https://cloud.google.com/docs/authentication/application-default-credentials for details.
38+
3139
; For installed application flow.
3240
clientId = "INSERT_OAUTH2_CLIENT_ID_HERE"
3341
clientSecret = "INSERT_OAUTH2_CLIENT_SECRET_HERE"

src/Google/Ads/GoogleAds/Lib/OAuth2TokenBuilder.php

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818

1919
namespace Google\Ads\GoogleAds\Lib;
2020

21+
use DomainException;
22+
use Google\Auth\CredentialsLoaderException;
23+
use Google\Auth\ApplicationDefaultCredentials;
2124
use Google\Auth\Credentials\ServiceAccountCredentials;
2225
use Google\Auth\Credentials\UserRefreshCredentials;
2326
use Google\Auth\FetchAuthTokenInterface;
2427
use InvalidArgumentException;
2528
use UnexpectedValueException;
29+
use Google\Ads\GoogleAds\Util\EnvironmentalVariables;
2630

2731
/**
2832
* Builds OAuth2 access token fetchers.
@@ -31,6 +35,8 @@
3135
*/
3236
final class OAuth2TokenBuilder extends AbstractGoogleAdsBuilder
3337
{
38+
private const DEFAULT_SCOPE = 'https://www.googleapis.com/auth/adwords';
39+
3440
private $jsonKeyFilePath;
3541
private $scopes;
3642
private $impersonatedEmail;
@@ -39,6 +45,16 @@ final class OAuth2TokenBuilder extends AbstractGoogleAdsBuilder
3945
private $clientSecret;
4046
private $refreshToken;
4147

48+
private $adcFetcher;
49+
50+
public function __construct(
51+
ConfigurationLoader $configurationLoader = null,
52+
?EnvironmentalVariables $environmentalVariables = null,
53+
) {
54+
parent::__construct($configurationLoader, $environmentalVariables);
55+
$this->adcFetcher = [ApplicationDefaultCredentials::class, 'getCredentials'];
56+
}
57+
4258
/**
4359
* @see GoogleAdsBuilder::from()
4460
*/
@@ -150,28 +166,84 @@ public function withImpersonatedEmail(?string $impersonatedEmail): self
150166
return $this;
151167
}
152168

169+
/**
170+
* Overrides the internal Application Default Credentials fetcher for testing purposes.
171+
* @param callable $adcFetcher The mock or custom callable.
172+
* @return self
173+
*/
174+
protected function withAdcFetcher(callable $adcFetcher): self
175+
{
176+
$this->adcFetcher = $adcFetcher;
177+
return $this;
178+
}
179+
153180
/**
154181
* @see GoogleAdsBuilder::build()
155182
*
156183
* @return FetchAuthTokenInterface the created OAuth2 object that can fetch auth tokens
157184
*/
158-
public function build()
185+
public function build(): FetchAuthTokenInterface
159186
{
160187
$this->defaultOptionals();
161-
$this->validate();
162188

163-
if ($this->jsonKeyFilePath !== null) {
189+
190+
191+
// 1. Check for **EXPLICIT** Service Account Flow
192+
if (!empty($this->jsonKeyFilePath)) {
193+
if (is_null($this->scopes)) {
194+
throw new InvalidArgumentException(
195+
"Both 'jsonKeyFilePath' and 'scopes' must be set when using service account flow."
196+
);
197+
}
198+
if (
199+
!is_null($this->clientId)
200+
|| !is_null($this->clientSecret)
201+
|| !is_null($this->refreshToken)
202+
) {
203+
throw new InvalidArgumentException(
204+
"Cannot have both service account flow and installed/web "
205+
. "application flow credential values set."
206+
);
207+
}
208+
// Service Account flow uses the specific configured scope string
164209
return new ServiceAccountCredentials(
165210
$this->scopes,
166211
$this->jsonKeyFilePath,
167212
$this->impersonatedEmail
168213
);
169-
} else {
170-
return new UserRefreshCredentials(null, [
171-
'client_id' => $this->clientId,
172-
'client_secret' => $this->clientSecret,
173-
'refresh_token' => $this->refreshToken
174-
]);
214+
}
215+
216+
// 2. Check for **EXPLICIT** User Refresh Token Flow (Installed/Web App)
217+
if (!empty($this->refreshToken)) {
218+
if (is_null($this->clientId) || is_null($this->clientSecret)) {
219+
throw new UnexpectedValueException(
220+
"Both 'clientId' and 'clientSecret' must be set when using 'refreshToken'."
221+
);
222+
}
223+
return new UserRefreshCredentials(
224+
// Use the determined scope array, allowing configuration via $this->scopes
225+
$this->scopes,
226+
[
227+
'client_id' => $this->clientId,
228+
'client_secret' => $this->clientSecret,
229+
'refresh_token' => $this->refreshToken
230+
]
231+
);
232+
}
233+
234+
// 3. FALLBACK: Use Application Default Credentials (ADC)
235+
try {
236+
// Use the determined scope array, allowing configuration via $this->scopes
237+
return call_user_func($this->adcFetcher, $this->scopes);
238+
} catch (CredentialsLoaderException $e) {
239+
throw new DomainException(
240+
"No OAuth2 credentials were provided, and the automatic Application Default "
241+
. "Credentials (ADC) search failed. Please ensure you have run "
242+
. "'gcloud auth application-default login' or set explicit credentials. "
243+
. "Underlying error: " . $e->getMessage(),
244+
0,
245+
$e
246+
);
175247
}
176248
}
177249

@@ -180,7 +252,12 @@ public function build()
180252
*/
181253
public function defaultOptionals()
182254
{
183-
// Nothing to default for this builder.
255+
// If the user has not set a custom scope via config or withScopes(),
256+
// default to the mandatory Google Ads API scope. This is needed for the
257+
// User Refresh Token and ADC fallback flows.
258+
if (is_null($this->scopes)) {
259+
$this->scopes = self::DEFAULT_SCOPE;
260+
}
184261
}
185262

186263
/**
@@ -191,29 +268,32 @@ public function validate()
191268
if (
192269
(!is_null($this->jsonKeyFilePath) || !is_null($this->scopes))
193270
&& (!is_null($this->clientId) || !is_null($this->clientSecret)
194-
|| !is_null($this->refreshToken))
271+
|| !is_null($this->refreshToken))
195272
) {
196273
throw new InvalidArgumentException(
197274
'Cannot have both service account '
198275
. 'flow and installed/web application flow credential values set.'
199276
);
200277
}
201-
if (!is_null($this->jsonKeyFilePath) || !is_null($this->scopes)) {
202-
if (is_null($this->jsonKeyFilePath) || is_null($this->scopes)) {
278+
if (!is_null($this->jsonKeyFilePath)) {
279+
if ($this->scopes === self::DEFAULT_SCOPE) {
203280
throw new InvalidArgumentException(
204281
"Both 'jsonKeyFilePath' and "
205282
. "'scopes' must be set when using service account flow."
206283
);
207284
}
285+
// Triggers validation if any part of the Installed/Web flow is set; otherwise, allows the ADC fallback.
208286
} elseif (
209-
is_null($this->clientId)
210-
|| is_null($this->clientSecret)
211-
|| is_null($this->refreshToken)
287+
!is_null($this->clientId)
288+
|| !is_null($this->clientSecret)
289+
|| !is_null($this->refreshToken)
212290
) {
213-
throw new UnexpectedValueException(
214-
"All of 'clientId', 'clientSecret', and 'refreshToken' must be set when using "
215-
. "installed/web application flow."
216-
);
291+
if ((is_null($this->clientId) || is_null($this->clientSecret) || is_null($this->refreshToken))) {
292+
throw new UnexpectedValueException(
293+
"All of 'clientId', 'clientSecret', and 'refreshToken' must be set when using "
294+
. "installed/web application flow."
295+
);
296+
}
217297
}
218298
}
219299

0 commit comments

Comments
 (0)