44
55namespace Keboola \Google \ClientBundle \Google ;
66
7+ use Google \Auth \CredentialsLoader ;
78use GuzzleHttp \Client ;
89use GuzzleHttp \Handler \CurlHandler ;
910use GuzzleHttp \HandlerStack ;
1011use GuzzleHttp \Psr7 \Response ;
1112use Keboola \Google \ClientBundle \Exception \RestApiException ;
1213use Keboola \Google \ClientBundle \Guzzle \RetryCallbackMiddleware ;
13- use Monolog \Logger ;
1414use Psr \Http \Message \RequestInterface ;
1515use Psr \Http \Message \ResponseInterface ;
16+ use Psr \Log \LoggerInterface ;
17+ use Throwable ;
1618
1719class RestApi
1820{
@@ -21,17 +23,20 @@ class RestApi
2123 private const DEFAULT_CONNECT_TIMEOUT = 30 ;
2224 private const DEFAULT_REQUEST_TIMEOUT = 5 * 60 ;
2325
26+ public const AUTH_TYPE_OAUTH = 'oauth ' ;
27+ public const AUTH_TYPE_SERVICE_ACCOUNT = 'service_account ' ;
28+
2429 /** @var int */
2530 protected $ maxBackoffs = 7 ;
2631
2732 /** @var callable */
2833 protected $ backoffCallback403 ;
2934
3035 /** @var string */
31- protected $ accessToken ;
36+ protected $ accessToken = '' ;
3237
3338 /** @var string */
34- protected $ refreshToken ;
39+ protected $ refreshToken = '' ;
3540
3641 /** @var string */
3742 protected $ clientId ;
@@ -45,24 +50,125 @@ class RestApi
4550 /** @var callable */
4651 protected $ delayFn = null ;
4752
48- /** @var ?Logger */
53+ /** @var ?LoggerInterface */
4954 protected $ logger ;
5055
51- public function __construct (
56+ /** @var string */
57+ protected $ authType = self ::AUTH_TYPE_OAUTH ;
58+
59+ /** @var ?array */
60+ protected $ serviceAccountConfig ;
61+
62+ /** @var ?array<string> */
63+ protected $ scopes ;
64+
65+ /** @var mixed */
66+ protected $ serviceAccountCredentials ;
67+
68+ /** @var ?string */
69+ protected $ serviceAccountAccessToken ;
70+
71+ /** @var ?int */
72+ protected $ serviceAccountTokenExpiry ;
73+
74+ public function __construct (?LoggerInterface $ logger = null )
75+ {
76+ $ this ->logger = $ logger ;
77+
78+ $ this ->backoffCallback403 = function () {
79+ return true ;
80+ };
81+ }
82+
83+ /**
84+ * Factory method for creating REST API client with OAuth authentication
85+ */
86+ public static function createWithOAuth (
5287 string $ clientId ,
5388 string $ clientSecret ,
5489 string $ accessToken = '' ,
5590 string $ refreshToken = '' ,
56- ?Logger $ logger = null ,
57- ) {
58- $ this ->clientId = $ clientId ;
59- $ this ->clientSecret = $ clientSecret ;
60- $ this ->setCredentials ($ accessToken , $ refreshToken );
61- $ this ->logger = $ logger ;
62-
63- $ this ->backoffCallback403 = function () {
91+ ?LoggerInterface $ logger = null ,
92+ ): self {
93+ $ instance = new self ($ logger );
94+ $ instance ->authType = self ::AUTH_TYPE_OAUTH ;
95+ $ instance ->clientId = $ clientId ;
96+ $ instance ->clientSecret = $ clientSecret ;
97+ $ instance ->setCredentials ($ accessToken , $ refreshToken );
98+
99+ $ instance ->backoffCallback403 = function () {
64100 return true ;
65101 };
102+
103+ return $ instance ;
104+ }
105+
106+ /**
107+ * Factory method for creating REST API client with Service Account authentication
108+ */
109+ public static function createWithServiceAccount (
110+ array $ serviceAccountConfig ,
111+ array $ scopes ,
112+ ?LoggerInterface $ logger = null ,
113+ ): self {
114+ $ instance = new self ($ logger );
115+ $ instance ->authType = self ::AUTH_TYPE_SERVICE_ACCOUNT ;
116+ $ instance ->serviceAccountConfig = $ serviceAccountConfig ;
117+ $ instance ->scopes = $ scopes ;
118+ $ instance ->initializeServiceAccountCredentials ();
119+ return $ instance ;
120+ }
121+
122+ /**
123+ * Initialize Service Account credentials using Google Auth SDK
124+ */
125+ protected function initializeServiceAccountCredentials (): void
126+ {
127+ if ($ this ->serviceAccountConfig === null || empty ($ this ->scopes )) {
128+ throw new RestApiException ('Service account configuration and scopes are required ' , 400 );
129+ }
130+
131+ try {
132+ $ this ->serviceAccountCredentials = CredentialsLoader::makeCredentials (
133+ $ this ->scopes ,
134+ $ this ->serviceAccountConfig ,
135+ );
136+ } catch (Throwable $ e ) {
137+ throw new RestApiException ('Failed to initialize service account credentials: ' . $ e ->getMessage (), 500 );
138+ }
139+ }
140+
141+ /**
142+ * Get access token for Service Account
143+ */
144+ protected function getServiceAccountAccessToken (): string
145+ {
146+ // Return cached token if still valid
147+ if ($ this ->serviceAccountAccessToken !== null &&
148+ $ this ->serviceAccountTokenExpiry !== null &&
149+ time () < $ this ->serviceAccountTokenExpiry - 60 ) { // 60s buffer
150+ return $ this ->serviceAccountAccessToken ;
151+ }
152+
153+ if ($ this ->serviceAccountCredentials === null ) {
154+ throw new RestApiException ('Service account credentials not initialized ' , 500 );
155+ }
156+
157+ try {
158+ // Fetch new access token using Google Auth SDK
159+ $ authToken = $ this ->serviceAccountCredentials ->fetchAuthToken ();
160+
161+ if (!isset ($ authToken ['access_token ' ])) {
162+ throw new RestApiException ('Failed to retrieve access token from service account ' , 500 );
163+ }
164+
165+ $ this ->serviceAccountAccessToken = $ authToken ['access_token ' ];
166+ $ this ->serviceAccountTokenExpiry = time () + ($ authToken ['expires_in ' ] ?? 3600 );
167+
168+ return $ this ->serviceAccountAccessToken ;
169+ } catch (Throwable $ e ) {
170+ throw new RestApiException ('Failed to fetch service account access token: ' . $ e ->getMessage (), 500 );
171+ }
66172 }
67173
68174 public static function createRetryMiddleware (
@@ -100,8 +206,16 @@ public function createRetryCallback(): callable
100206 ?ResponseInterface $ response = null ,
101207 ) use ($ api ) {
102208 if ($ response && $ response ->getStatusCode () === 401 ) {
103- $ tokens = $ api ->refreshToken ();
104- return $ request ->withHeader ('Authorization ' , 'Bearer ' . $ tokens ['access_token ' ]);
209+ if ($ api ->authType === self ::AUTH_TYPE_SERVICE_ACCOUNT ) {
210+ // For Service Account, get new access token
211+ $ api ->serviceAccountAccessToken = null ; // Clear cache
212+ $ accessToken = $ api ->getServiceAccountAccessToken ();
213+ return $ request ->withHeader ('Authorization ' , 'Bearer ' . $ accessToken );
214+ } else {
215+ // For OAuth, use refresh token
216+ $ tokens = $ api ->refreshToken ();
217+ return $ request ->withHeader ('Authorization ' , 'Bearer ' . $ tokens ['access_token ' ]);
218+ }
105219 }
106220 return $ request ;
107221 };
@@ -148,14 +262,27 @@ public function setCredentials(string $accessToken, string $refreshToken): void
148262
149263 public function getAccessToken (): string
150264 {
265+ if ($ this ->authType === self ::AUTH_TYPE_SERVICE_ACCOUNT ) {
266+ return $ this ->getServiceAccountAccessToken ();
267+ }
268+
151269 return $ this ->accessToken ;
152270 }
153271
154272 public function getRefreshToken (): array
155273 {
274+ if ($ this ->authType === self ::AUTH_TYPE_SERVICE_ACCOUNT ) {
275+ throw new RestApiException ('Refresh token is not applicable for service account authentication ' , 400 );
276+ }
277+
156278 return $ this ->refreshToken ();
157279 }
158280
281+ public function getAuthType (): string
282+ {
283+ return $ this ->authType ;
284+ }
285+
159286 public function setRefreshTokenCallback (callable $ callback ): void
160287 {
161288 $ this ->refreshTokenCallback = $ callback ;
@@ -260,13 +387,21 @@ public function request(
260387 throw new RestApiException ('Wrong http method specified ' , 500 );
261388 }
262389
263- if ($ this ->refreshToken === null ) {
264- throw new RestApiException ('Refresh token must be set ' , 400 );
390+ // Validate authentication based on type
391+ if ($ this ->authType === self ::AUTH_TYPE_OAUTH && $ this ->refreshToken === null ) {
392+ throw new RestApiException ('Refresh token must be set for OAuth authentication ' , 400 );
393+ } elseif ($ this ->authType === self ::AUTH_TYPE_SERVICE_ACCOUNT &&
394+ ($ this ->serviceAccountConfig === null || $ this ->scopes === null || empty ($ this ->scopes ))
395+ ) {
396+ throw new RestApiException (
397+ 'Service account configuration and scopes must be set for service account authentication ' ,
398+ 400 ,
399+ );
265400 }
266401
267402 $ headers = [
268403 'Accept ' => 'application/json ' ,
269- 'Authorization ' => 'Bearer ' . $ this ->accessToken ,
404+ 'Authorization ' => 'Bearer ' . $ this ->getAccessToken () ,
270405 ];
271406
272407 foreach ($ addHeaders as $ k => $ v ) {
@@ -306,11 +441,11 @@ protected function logRetryRequest(
306441 ];
307442
308443 $ this ->logger ->info (
309- sprintf ('Retrying request (%sx ) - reason: %s ' , $ retries , $ response ->getReasonPhrase ()),
444+ sprintf ('Retrying request (%dx ) - reason: %s ' , $ retries , $ response ->getReasonPhrase ()),
310445 $ context ,
311446 );
312447 } else {
313- $ this ->logger ->info (sprintf ('Retrying request (%sx ) ' , $ retries ), $ context );
448+ $ this ->logger ->info (sprintf ('Retrying request (%dx ) ' , $ retries ), $ context );
314449 }
315450 }
316451 }
0 commit comments