From 0a6669253d7210837c9125927100a612f98eeaf4 Mon Sep 17 00:00:00 2001 From: Tom Birch Date: Thu, 30 Apr 2026 18:00:04 +1000 Subject: [PATCH] fix(providers): refresh GCP metadata server token on expiration The GCP metadata server provides a token that expires, but the original implementation of DefaultAccount in GcpAuth was caching the response of the metadata server directly in the credentials structure instead of fetching a new token when needed. This effectively created an infinite-lived default credential until Goose restarted, at which point it'd read the metadata server again. I have modified AdcCredentials::DefaultAccount to store the base URL instead of the one-time token, and get_default_access_token to actually re-fetch the token using the URL so the standard caching mechanism works properly and automatically refreshes it on expiration. Signed-off-by: Tom Birch --- crates/goose/src/providers/gcpauth.rs | 56 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/crates/goose/src/providers/gcpauth.rs b/crates/goose/src/providers/gcpauth.rs index 16d2956fda91..448249b8b7ae 100644 --- a/crates/goose/src/providers/gcpauth.rs +++ b/crates/goose/src/providers/gcpauth.rs @@ -58,7 +58,8 @@ enum AdcCredentials { /// Credentials for a service account ServiceAccount(ServiceAccountCredentials), /// Credentials for the GCP native default account - DefaultAccount(TokenResponse), + #[serde(skip)] + DefaultAccount(String), } /// Credentials for an authorized user account. @@ -258,19 +259,11 @@ impl AdcCredentials { )); } - // Get the identity token and credentials from metadata server - let token_response = response - .json::() - .await - .map_err(|e| AuthError::Credentials(format!("Invalid metadata response: {}", e)))?; - // Note: When using metadata server, we have access to the OAuth2 access token // that can be used to authenticate applications. - Ok(AdcCredentials::DefaultAccount(TokenResponse { - token_type: token_response.token_type, - access_token: token_response.access_token, - expires_in: token_response.expires_in, - })) + // However, this token expires. Better to keep the base_url, and fetch a new token + // when needed + Ok(AdcCredentials::DefaultAccount(base_url.to_string())) } } @@ -539,15 +532,36 @@ impl GcpAuth { /// Gets a token directly from the GCP metadata endpoint. /// /// # Arguments - /// * `creds` - Default Access Token Response + /// * `base_url` - Default Access Token base url /// /// # Returns /// * `Result` - The token response - async fn get_default_access_token( - &self, - creds: &TokenResponse, - ) -> Result { - Ok(creds.clone()) + async fn get_default_access_token(&self, base_url: &str) -> Result { + let metadata_path = "/computeMetadata/v1/instance/service-accounts/default/token"; + let response = self + .client + .get(format!("{}{}", base_url, metadata_path)) + .header("Metadata-Flavor", "Google") + .send() + .await + .map_err(|e| AuthError::TokenExchange(e.to_string()))?; + + let status = response.status(); + if !status.is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + return Err(AuthError::TokenExchange(format!( + "Status {}: {}", + status, error_text + ))); + } + + response + .json::() + .await + .map_err(|e| AuthError::TokenExchange(format!("Invalid response: {}", e))) } } @@ -1019,10 +1033,8 @@ iXVBc2YmAuU8hiOFUPxtyQfNzG5fQ0rhJSewdtyWxIadJSLj6fsK+AEsNQ== result ); - if let Ok(AdcCredentials::DefaultAccount(token_response)) = result { - assert_eq!(token_response.access_token, expected_token); - assert_eq!(token_response.token_type, expected_type); - assert_eq!(token_response.expires_in, expected_expires); + if let Ok(AdcCredentials::DefaultAccount(base_url)) = result { + assert_eq!(base_url, mock_server.uri()); } else { panic!("Expected DefaultAccount credentials, got {:?}", result); }