1818
1919namespace Google \Ads \GoogleAds \Lib ;
2020
21+ use DomainException ;
22+ use Google \Auth \CredentialsLoaderException ;
23+ use Google \Auth \ApplicationDefaultCredentials ;
2124use Google \Auth \Credentials \ServiceAccountCredentials ;
2225use Google \Auth \Credentials \UserRefreshCredentials ;
2326use Google \Auth \FetchAuthTokenInterface ;
2427use InvalidArgumentException ;
2528use UnexpectedValueException ;
29+ use Google \Ads \GoogleAds \Util \EnvironmentalVariables ;
2630
2731/**
2832 * Builds OAuth2 access token fetchers.
3135 */
3236final 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