5151import com .google .common .base .Joiner ;
5252import com .google .common .base .MoreObjects .ToStringHelper ;
5353import com .google .common .collect .ImmutableList ;
54- import com .google .common .collect .ImmutableMap ;
5554import com .google .common .collect .ImmutableSet ;
5655import com .google .errorprone .annotations .CanIgnoreReturnValue ;
5756import java .io .BufferedReader ;
@@ -129,6 +128,7 @@ public class ComputeEngineCredentials extends GoogleCredentials
129128 private transient HttpTransportFactory transportFactory ;
130129
131130 private String universeDomainFromMetadata = null ;
131+ private String projectId = null ;
132132
133133 /**
134134 * Experimental Feature.
@@ -340,6 +340,81 @@ private String getUniverseDomainFromMetadata() throws IOException {
340340 return responseString ;
341341 }
342342
343+ /**
344+ * Retrieves the Google Cloud project ID from the Compute Engine (GCE) metadata server.
345+ *
346+ * <p>On its first successful execution, it fetches the project ID and caches it for the lifetime
347+ * of the object. Subsequent calls will return the cached value without making additional network
348+ * requests.
349+ *
350+ * <p>If the request to the metadata server fails (e.g., due to network issues, or if the VM lacks
351+ * the required service account permissions), the method will attempt to fall back to a default
352+ * project ID provider which could be {@code null}.
353+ *
354+ * @return the GCP project ID string, or {@code null} if the metadata server is inaccessible and
355+ * no fallback project ID can be determined.
356+ */
357+ @ Override
358+ public String getProjectId () {
359+ synchronized (this ) {
360+ if (this .projectId != null ) {
361+ return this .projectId ;
362+ }
363+ }
364+
365+ String projectIdFromMetadata = getProjectIdFromMetadata ();
366+ synchronized (this ) {
367+ // Check first if another thread set the Project ID. No need to overwrite
368+ // if a Projects ID already exists. Tries to prevent a case where the last call
369+ // for `getProjectIdFromMetadata()` returns null and overwrites valid data.
370+ if (this .projectId == null ) {
371+ this .projectId = projectIdFromMetadata ;
372+ }
373+ }
374+ return this .projectId ;
375+ }
376+
377+ private String getProjectIdFromMetadata () {
378+ try {
379+ HttpResponse response = getMetadataResponse (getProjectIdUrl (), RequestType .UNTRACKED , false );
380+ int statusCode = response .getStatusCode ();
381+ if (statusCode == HttpStatusCodes .STATUS_CODE_NOT_FOUND ) {
382+ LoggingUtils .log (
383+ LOGGER_PROVIDER ,
384+ Level .WARNING ,
385+ Collections .emptyMap (),
386+ String .format (
387+ "Error code %s trying to get project ID from"
388+ + " Compute Engine metadata. This may be because the virtual machine instance"
389+ + " does not have permission scopes specified." ,
390+ statusCode ));
391+ return super .getProjectId ();
392+ }
393+ if (statusCode != HttpStatusCodes .STATUS_CODE_OK ) {
394+ LoggingUtils .log (
395+ LOGGER_PROVIDER ,
396+ Level .WARNING ,
397+ Collections .emptyMap (),
398+ String .format (
399+ "Unexpected Error code %s trying to get project ID"
400+ + " from Compute Engine metadata for the default service account: %s" ,
401+ statusCode , response .parseAsString ()));
402+ return super .getProjectId ();
403+ }
404+ return response .parseAsString ();
405+ } catch (IOException e ) {
406+ LoggingUtils .log (
407+ LOGGER_PROVIDER ,
408+ Level .WARNING ,
409+ Collections .emptyMap (),
410+ String .format (
411+ "Unexpected Error: %s trying to get project ID"
412+ + " from Compute Engine metadata server. Reason: %s" ,
413+ e .getMessage (), e .getCause ().toString ()));
414+ return super .getProjectId ();
415+ }
416+ }
417+
343418 /** Refresh the access token by getting it from the GCE metadata server */
344419 @ Override
345420 public AccessToken refreshAccessToken () throws IOException {
@@ -435,11 +510,9 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
435510 }
436511 String rawToken = response .parseAsString ();
437512
438- LoggingUtils .log (
439- LOGGER_PROVIDER ,
440- Level .FINE ,
441- ImmutableMap .of ("idToken" , rawToken ),
442- "Response Payload for ID token" );
513+ GenericData idTokenData = new GenericData ();
514+ idTokenData .set ("id_token" , rawToken );
515+ LoggingUtils .logResponsePayload (idTokenData , LOGGER_PROVIDER , "Response Payload for ID token" );
443516 return IdToken .create (rawToken );
444517 }
445518
@@ -448,6 +521,9 @@ private HttpResponse getMetadataResponse(
448521 GenericUrl genericUrl = new GenericUrl (url );
449522 HttpRequest request =
450523 transportFactory .create ().createRequestFactory ().buildGetRequest (genericUrl );
524+ // Disable automatic logging by google-http-java-client to prevent leakage of sensitive tokens.
525+ // Client Library Debug Logging via LoggingUtils is used instead where appropriate.
526+ request .setLoggingEnabled (false );
451527 JsonObjectParser parser = new JsonObjectParser (OAuth2Utils .JSON_FACTORY );
452528 request .setParser (parser );
453529 request .getHeaders ().set (METADATA_FLAVOR , GOOGLE );
@@ -461,17 +537,17 @@ private HttpResponse getMetadataResponse(
461537 request .setThrowExceptionOnExecuteError (false );
462538 HttpResponse response ;
463539 try {
464- String requestMessage ;
465- String responseMessage ;
540+ String requestMessage = null ;
541+ String responseMessage = null ;
466542 if (requestType .equals (RequestType .ID_TOKEN_REQUEST )) {
467543 requestMessage = "Sending request to get ID token" ;
468544 responseMessage = "Received response for ID token request" ;
469545 } else if (requestType .equals (RequestType .ACCESS_TOKEN_REQUEST )) {
470546 requestMessage = "Sending request to refresh access token" ;
471547 responseMessage = "Received response for refresh access token" ;
472548 } else {
473- // TODO: this includes get universe domain and get default sa.
474- // refactor for more clear logging message.
549+ // TODO: this includes get universe domain and get default sa. Refactor for more clear
550+ // logging message.
475551 requestMessage = "Sending request for universe domain/default service account" ;
476552 responseMessage = "Received response for universe domain/default service account" ;
477553 }
@@ -564,13 +640,21 @@ static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) {
564640 return false ;
565641 }
566642
643+ @ VisibleForTesting
644+ void setProjectId (String projectId ) {
645+ this .projectId = projectId ;
646+ }
647+
567648 private static boolean pingComputeEngineMetadata (
568649 HttpTransportFactory transportFactory , DefaultCredentialsProvider provider ) {
569650 GenericUrl tokenUrl = new GenericUrl (getMetadataServerUrl (provider ));
570651 for (int i = 1 ; i <= MAX_COMPUTE_PING_TRIES ; ++i ) {
571652 try {
572653 HttpRequest request =
573654 transportFactory .create ().createRequestFactory ().buildGetRequest (tokenUrl );
655+ // Disable automatic logging by google-http-java-client. This is a ping request
656+ // and does not need to be logged by LoggingUtils.
657+ request .setLoggingEnabled (false );
574658 request .setConnectTimeout (COMPUTE_PING_CONNECTION_TIMEOUT_MS );
575659 request .getHeaders ().set (METADATA_FLAVOR , GOOGLE );
576660 MetricsUtils .setMetricsHeader (
@@ -642,6 +726,11 @@ public static String getIdentityDocumentUrl() {
642726 + "/computeMetadata/v1/instance/service-accounts/default/identity" ;
643727 }
644728
729+ public static String getProjectIdUrl () {
730+ return getMetadataServerUrl (DefaultCredentialsProvider .DEFAULT )
731+ + "/computeMetadata/v1/project/project-id" ;
732+ }
733+
645734 @ Override
646735 public int hashCode () {
647736 return Objects .hash (transportFactoryClassName );
0 commit comments