fix(providers): refresh GCP metadata server token on expiration (#8929)

Signed-off-by: Tom Birch <tom@neara.com>
Co-authored-by: Tom Birch <tom@neara.com>
This commit is contained in:
Tom Birch
2026-05-14 02:16:11 +10:00
committed by GitHub
parent 2edfc3075d
commit 81c59e013d
+34 -22
View File
@@ -58,7 +58,8 @@ enum AdcCredentials {
/// Credentials for a service account /// Credentials for a service account
ServiceAccount(ServiceAccountCredentials), ServiceAccount(ServiceAccountCredentials),
/// Credentials for the GCP native default account /// Credentials for the GCP native default account
DefaultAccount(TokenResponse), #[serde(skip)]
DefaultAccount(String),
} }
/// Credentials for an authorized user account. /// 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::<TokenResponse>()
.await
.map_err(|e| AuthError::Credentials(format!("Invalid metadata response: {}", e)))?;
// Note: When using metadata server, we have access to the OAuth2 access token // Note: When using metadata server, we have access to the OAuth2 access token
// that can be used to authenticate applications. // that can be used to authenticate applications.
Ok(AdcCredentials::DefaultAccount(TokenResponse { // However, this token expires. Better to keep the base_url, and fetch a new token
token_type: token_response.token_type, // when needed
access_token: token_response.access_token, Ok(AdcCredentials::DefaultAccount(base_url.to_string()))
expires_in: token_response.expires_in,
}))
} }
} }
@@ -539,15 +532,36 @@ impl GcpAuth {
/// Gets a token directly from the GCP metadata endpoint. /// Gets a token directly from the GCP metadata endpoint.
/// ///
/// # Arguments /// # Arguments
/// * `creds` - Default Access Token Response /// * `base_url` - Default Access Token base url
/// ///
/// # Returns /// # Returns
/// * `Result<TokenResponse>` - The token response /// * `Result<TokenResponse>` - The token response
async fn get_default_access_token( async fn get_default_access_token(&self, base_url: &str) -> Result<TokenResponse, AuthError> {
&self, let metadata_path = "/computeMetadata/v1/instance/service-accounts/default/token";
creds: &TokenResponse, let response = self
) -> Result<TokenResponse, AuthError> { .client
Ok(creds.clone()) .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::<TokenResponse>()
.await
.map_err(|e| AuthError::TokenExchange(format!("Invalid response: {}", e)))
} }
} }
@@ -1019,10 +1033,8 @@ iXVBc2YmAuU8hiOFUPxtyQfNzG5fQ0rhJSewdtyWxIadJSLj6fsK+AEsNQ==
result result
); );
if let Ok(AdcCredentials::DefaultAccount(token_response)) = result { if let Ok(AdcCredentials::DefaultAccount(base_url)) = result {
assert_eq!(token_response.access_token, expected_token); assert_eq!(base_url, mock_server.uri());
assert_eq!(token_response.token_type, expected_type);
assert_eq!(token_response.expires_in, expected_expires);
} else { } else {
panic!("Expected DefaultAccount credentials, got {:?}", result); panic!("Expected DefaultAccount credentials, got {:?}", result);
} }