• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015, Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *    * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *
15  *    * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 package com.google.auth.oauth2;
33 
34 import static com.google.common.base.MoreObjects.firstNonNull;
35 
36 import com.google.api.client.http.GenericUrl;
37 import com.google.api.client.http.HttpHeaders;
38 import com.google.api.client.http.HttpRequest;
39 import com.google.api.client.http.HttpResponse;
40 import com.google.api.client.http.HttpResponseException;
41 import com.google.api.client.http.HttpStatusCodes;
42 import com.google.api.client.json.JsonObjectParser;
43 import com.google.api.client.util.GenericData;
44 import com.google.auth.Credentials;
45 import com.google.auth.Retryable;
46 import com.google.auth.ServiceAccountSigner;
47 import com.google.auth.http.HttpTransportFactory;
48 import com.google.common.annotations.VisibleForTesting;
49 import com.google.common.base.Joiner;
50 import com.google.common.base.MoreObjects.ToStringHelper;
51 import com.google.common.collect.ImmutableSet;
52 import com.google.errorprone.annotations.CanIgnoreReturnValue;
53 import java.io.BufferedReader;
54 import java.io.File;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.InputStreamReader;
58 import java.io.ObjectInputStream;
59 import java.net.SocketTimeoutException;
60 import java.net.UnknownHostException;
61 import java.time.Duration;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.Date;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Objects;
70 import java.util.logging.Level;
71 import java.util.logging.Logger;
72 
73 /**
74  * OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
75  *
76  * <p>Fetches access tokens from the Google Compute Engine metadata server.
77  *
78  * <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
79  */
80 public class ComputeEngineCredentials extends GoogleCredentials
81     implements ServiceAccountSigner, IdTokenProvider {
82 
83   // Decrease timing margins on GCE.
84   // This is needed because GCE VMs maintain their own OAuth cache that expires T-4 mins, attempting
85   // to refresh a token before then, will yield the same stale token. To enable pre-emptive
86   // refreshes, the margins must be shortened. This shouldn't cause problems since the clock skew
87   // on the VM and metadata proxy should be non-existent.
88   static final Duration COMPUTE_EXPIRATION_MARGIN = Duration.ofMinutes(3);
89   static final Duration COMPUTE_REFRESH_MARGIN = Duration.ofMinutes(3).plusSeconds(45);
90 
91   private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());
92 
93   static final String DEFAULT_METADATA_SERVER_URL = "http://metadata.google.internal";
94 
95   static final String SIGN_BLOB_URL_FORMAT =
96       "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
97 
98   // Note: the explicit `timeout` and `tries` below is a workaround. The underlying
99   // issue is that resolving an unknown host on some networks will take
100   // 20-30 seconds; making this timeout short fixes the issue, but
101   // could lead to false negatives in the event that we are on GCE, but
102   // the metadata resolution was particularly slow. The latter case is
103   // "unlikely" since the expected 4-nines time is about 0.5 seconds.
104   // This allows us to limit the total ping maximum timeout to 1.5 seconds
105   // for developer desktop scenarios.
106   static final int MAX_COMPUTE_PING_TRIES = 3;
107   static final int COMPUTE_PING_CONNECTION_TIMEOUT_MS = 500;
108 
109   private static final String METADATA_FLAVOR = "Metadata-Flavor";
110   private static final String GOOGLE = "Google";
111   private static final String WINDOWS = "windows";
112   private static final String LINUX = "linux";
113 
114   private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
115   private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
116   private static final long serialVersionUID = -4113476462526554235L;
117 
118   private final String transportFactoryClassName;
119 
120   private final Collection<String> scopes;
121 
122   private transient HttpTransportFactory transportFactory;
123   private transient String serviceAccountEmail;
124 
125   private String universeDomainFromMetadata = null;
126 
127   /**
128    * An internal constructor
129    *
130    * @param builder A builder for {@link ComputeEngineCredentials} See {@link
131    *     ComputeEngineCredentials.Builder}
132    */
ComputeEngineCredentials(ComputeEngineCredentials.Builder builder)133   private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) {
134     super(builder);
135 
136     this.transportFactory =
137         firstNonNull(
138             builder.getHttpTransportFactory(),
139             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
140     this.transportFactoryClassName = this.transportFactory.getClass().getName();
141     // Use defaultScopes only when scopes don't exist.
142     Collection<String> scopesToUse = builder.scopes;
143     if (scopesToUse == null || scopesToUse.isEmpty()) {
144       scopesToUse = builder.getDefaultScopes();
145     }
146     if (scopesToUse == null) {
147       this.scopes = ImmutableSet.<String>of();
148     } else {
149       List<String> scopeList = new ArrayList<String>(scopesToUse);
150       scopeList.removeAll(Arrays.asList("", null));
151       this.scopes = ImmutableSet.<String>copyOf(scopeList);
152     }
153   }
154 
155   /** Clones the compute engine account with the specified scopes. */
156   @Override
createScoped(Collection<String> newScopes)157   public GoogleCredentials createScoped(Collection<String> newScopes) {
158     ComputeEngineCredentials.Builder builder =
159         this.toBuilder().setHttpTransportFactory(transportFactory).setScopes(newScopes);
160     return new ComputeEngineCredentials(builder);
161   }
162 
163   /** Clones the compute engine account with the specified scopes and default scopes. */
164   @Override
createScoped( Collection<String> newScopes, Collection<String> newDefaultScopes)165   public GoogleCredentials createScoped(
166       Collection<String> newScopes, Collection<String> newDefaultScopes) {
167     ComputeEngineCredentials.Builder builder =
168         ComputeEngineCredentials.newBuilder()
169             .setHttpTransportFactory(transportFactory)
170             .setScopes(newScopes)
171             .setDefaultScopes(newDefaultScopes);
172     return new ComputeEngineCredentials(builder);
173   }
174 
175   /**
176    * Create a new ComputeEngineCredentials instance with default behavior.
177    *
178    * @return new ComputeEngineCredentials
179    */
create()180   public static ComputeEngineCredentials create() {
181     return new ComputeEngineCredentials(ComputeEngineCredentials.newBuilder());
182   }
183 
getScopes()184   public final Collection<String> getScopes() {
185     return scopes;
186   }
187 
188   /**
189    * If scopes is specified, add "?scopes=comma-separated-list-of-scopes" to the token url.
190    *
191    * @return token url with the given scopes
192    */
createTokenUrlWithScopes()193   String createTokenUrlWithScopes() {
194     GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl());
195     if (!scopes.isEmpty()) {
196       tokenUrl.set("scopes", Joiner.on(',').join(scopes));
197     }
198     return tokenUrl.toString();
199   }
200 
201   /**
202    * Gets the universe domain from the GCE metadata server.
203    *
204    * <p>Returns an explicit universe domain if it was provided during credential initialization.
205    *
206    * <p>Returns the {@link Credentials#GOOGLE_DEFAULT_UNIVERSE} if universe domain endpoint is not
207    * found (404) or returns an empty string.
208    *
209    * <p>Otherwise, returns universe domain from GCE metadata service.
210    *
211    * <p>Any above value is cached for the credential lifetime.
212    *
213    * @throws IOException if a call to GCE metadata service was unsuccessful. Check if exception
214    *     implements the {@link Retryable} and {@code isRetryable()} will return true if the
215    *     operation may be retried.
216    * @return string representing a universe domain in the format some-domain.xyz
217    */
218   @Override
getUniverseDomain()219   public String getUniverseDomain() throws IOException {
220     if (isExplicitUniverseDomain()) {
221       return super.getUniverseDomain();
222     }
223 
224     synchronized (this) {
225       if (this.universeDomainFromMetadata != null) {
226         return this.universeDomainFromMetadata;
227       }
228     }
229 
230     String universeDomainFromMetadata = getUniverseDomainFromMetadata();
231     synchronized (this) {
232       this.universeDomainFromMetadata = universeDomainFromMetadata;
233     }
234     return universeDomainFromMetadata;
235   }
236 
getUniverseDomainFromMetadata()237   private String getUniverseDomainFromMetadata() throws IOException {
238     HttpResponse response = getMetadataResponse(getUniverseDomainUrl());
239     int statusCode = response.getStatusCode();
240     if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
241       return Credentials.GOOGLE_DEFAULT_UNIVERSE;
242     }
243     if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
244       IOException cause =
245           new IOException(
246               String.format(
247                   "Unexpected Error code %s trying to get universe domain"
248                       + " from Compute Engine metadata for the default service account: %s",
249                   statusCode, response.parseAsString()));
250       throw new GoogleAuthException(true, cause);
251     }
252     String responseString = response.parseAsString();
253 
254     /* Earlier versions of MDS that supports universe_domain return empty string instead of GDU. */
255     if (responseString.isEmpty()) {
256       return Credentials.GOOGLE_DEFAULT_UNIVERSE;
257     }
258     return responseString;
259   }
260 
261   /** Refresh the access token by getting it from the GCE metadata server */
262   @Override
refreshAccessToken()263   public AccessToken refreshAccessToken() throws IOException {
264     HttpResponse response = getMetadataResponse(createTokenUrlWithScopes());
265     int statusCode = response.getStatusCode();
266     if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
267       throw new IOException(
268           String.format(
269               "Error code %s trying to get security access token from"
270                   + " Compute Engine metadata for the default service account. This may be because"
271                   + " the virtual machine instance does not have permission scopes specified."
272                   + " It is possible to skip checking for Compute Engine metadata by specifying the environment "
273                   + " variable "
274                   + DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR
275                   + "=true.",
276               statusCode));
277     }
278     if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
279       throw new IOException(
280           String.format(
281               "Unexpected Error code %s trying to get security access"
282                   + " token from Compute Engine metadata for the default service account: %s",
283               statusCode, response.parseAsString()));
284     }
285     InputStream content = response.getContent();
286     if (content == null) {
287       // Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
288       // Mock transports will have success code with empty content by default.
289       throw new IOException("Empty content from metadata token server request.");
290     }
291     GenericData responseData = response.parseAs(GenericData.class);
292     String accessToken =
293         OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX);
294     int expiresInSeconds =
295         OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
296     long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
297     return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
298   }
299 
300   /**
301    * Returns a Google ID Token from the metadata server on ComputeEngine
302    *
303    * @param targetAudience the aud: field the IdToken should include
304    * @param options list of Credential specific options for the token. For example, an IDToken for a
305    *     ComputeEngineCredential could have the full formatted claims returned if
306    *     IdTokenProvider.Option.FORMAT_FULL) is provided as a list option. Valid option values are:
307    *     <br>
308    *     IdTokenProvider.Option.FORMAT_FULL<br>
309    *     IdTokenProvider.Option.LICENSES_TRUE<br>
310    *     If no options are set, the defaults are "&amp;format=standard&amp;licenses=false"
311    * @throws IOException if the attempt to get an IdToken failed
312    * @return IdToken object which includes the raw id_token, JsonWebSignature
313    */
314   @Override
idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)315   public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)
316       throws IOException {
317     GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl());
318     if (options != null) {
319       if (options.contains(IdTokenProvider.Option.FORMAT_FULL)) {
320         documentUrl.set("format", "full");
321       }
322       if (options.contains(IdTokenProvider.Option.LICENSES_TRUE)) {
323         // license will only get returned if format is also full
324         documentUrl.set("format", "full");
325         documentUrl.set("license", "TRUE");
326       }
327     }
328     documentUrl.set("audience", targetAudience);
329     HttpResponse response = getMetadataResponse(documentUrl.toString());
330     InputStream content = response.getContent();
331     if (content == null) {
332       throw new IOException("Empty content from metadata token server request.");
333     }
334     String rawToken = response.parseAsString();
335     return IdToken.create(rawToken);
336   }
337 
getMetadataResponse(String url)338   private HttpResponse getMetadataResponse(String url) throws IOException {
339     GenericUrl genericUrl = new GenericUrl(url);
340     HttpRequest request =
341         transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
342     JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
343     request.setParser(parser);
344     request.getHeaders().set(METADATA_FLAVOR, GOOGLE);
345     request.setThrowExceptionOnExecuteError(false);
346     HttpResponse response;
347     try {
348       response = request.execute();
349     } catch (UnknownHostException exception) {
350       throw new IOException(
351           "ComputeEngineCredentials cannot find the metadata server. This is"
352               + " likely because code is not running on Google Compute Engine.",
353           exception);
354     }
355 
356     if (response.getStatusCode() == 503) {
357       throw GoogleAuthException.createWithTokenEndpointResponseException(
358           new HttpResponseException(response));
359     }
360 
361     return response;
362   }
363 
364   /**
365    * Implements an algorithm to detect whether the code is running on Google Compute Environment
366    * (GCE) or equivalent runtime. <a href="https://google.aip.dev/auth/4115">See AIP-4115 for more
367    * details</a> The algorithm consists of active and passive checks: <br>
368    * <b>Active:</b> to check that GCE Metadata service is present by sending a http request to send
369    * a request to {@code ComputeEngineCredentials.DEFAULT_METADATA_SERVER_URL}
370    *
371    * <p><b>Passive:</b> to check if SMBIOS variable is present and contains expected value. This
372    * step is platform specific:
373    *
374    * <p><b>For Linux:</b> check if the file "/sys/class/dmi/id/product_name" exists and contains a
375    * line that starts with Google.
376    *
377    * <p><b>For Windows:</b> to be implemented
378    *
379    * <p><b>Other platforms:</b> not supported
380    *
381    * <p>This algorithm can be disabled with environment variable {@code
382    * DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR} set to {@code true}. In this case, the
383    * algorithm will always return {@code false} Returns {@code true} if currently running on Google
384    * Compute Environment (GCE) or equivalent runtime. Returns {@code false} if detection fails,
385    * platform is not supported or if detection disabled using the environment variable.
386    */
isOnGce( HttpTransportFactory transportFactory, DefaultCredentialsProvider provider)387   static synchronized boolean isOnGce(
388       HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
389     // If the environment has requested that we do no GCE checks, return immediately.
390     if (Boolean.parseBoolean(provider.getEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR))) {
391       return false;
392     }
393 
394     boolean result = pingComputeEngineMetadata(transportFactory, provider);
395 
396     if (!result) {
397       result = checkStaticGceDetection(provider);
398     }
399 
400     if (!result) {
401       LOGGER.log(Level.FINE, "Failed to detect whether running on Google Compute Engine.");
402     }
403 
404     return result;
405   }
406 
407   @VisibleForTesting
checkProductNameOnLinux(BufferedReader reader)408   static boolean checkProductNameOnLinux(BufferedReader reader) throws IOException {
409     String name = reader.readLine().trim();
410     return name.startsWith(GOOGLE);
411   }
412 
413   @VisibleForTesting
checkStaticGceDetection(DefaultCredentialsProvider provider)414   static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) {
415     String osName = provider.getOsName();
416     try {
417       if (osName.startsWith(LINUX)) {
418         // Checks GCE residency on Linux platform.
419         File linuxFile = new File("/sys/class/dmi/id/product_name");
420         return checkProductNameOnLinux(
421             new BufferedReader(new InputStreamReader(provider.readStream(linuxFile))));
422       } else if (osName.startsWith(WINDOWS)) {
423         // Checks GCE residency on Windows platform.
424         // TODO: implement registry check via FFI
425         return false;
426       }
427     } catch (IOException e) {
428       LOGGER.log(Level.FINE, "Encountered an unexpected exception when checking SMBIOS value", e);
429       return false;
430     }
431     // Platforms other than Linux and Windows are not supported.
432     return false;
433   }
434 
pingComputeEngineMetadata( HttpTransportFactory transportFactory, DefaultCredentialsProvider provider)435   private static boolean pingComputeEngineMetadata(
436       HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
437     GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider));
438     for (int i = 1; i <= MAX_COMPUTE_PING_TRIES; ++i) {
439       try {
440         HttpRequest request =
441             transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
442         request.setConnectTimeout(COMPUTE_PING_CONNECTION_TIMEOUT_MS);
443         request.getHeaders().set(METADATA_FLAVOR, GOOGLE);
444 
445         HttpResponse response = request.execute();
446         try {
447           // Internet providers can return a generic response to all requests, so it is necessary
448           // to check that metadata header is present also.
449           HttpHeaders headers = response.getHeaders();
450           return OAuth2Utils.headersContainValue(headers, METADATA_FLAVOR, GOOGLE);
451         } finally {
452           response.disconnect();
453         }
454       } catch (SocketTimeoutException expected) {
455         // Ignore logging timeouts which is the expected failure mode in non GCE environments.
456       } catch (IOException e) {
457         LOGGER.log(
458             Level.FINE,
459             "Encountered an unexpected exception when checking"
460                 + " if running on Google Compute Engine using Metadata Service ping.",
461             e);
462       }
463     }
464     return false;
465   }
466 
getMetadataServerUrl(DefaultCredentialsProvider provider)467   public static String getMetadataServerUrl(DefaultCredentialsProvider provider) {
468     String metadataServerAddress =
469         provider.getEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR);
470     if (metadataServerAddress != null) {
471       return "http://" + metadataServerAddress;
472     }
473     return DEFAULT_METADATA_SERVER_URL;
474   }
475 
getMetadataServerUrl()476   public static String getMetadataServerUrl() {
477     return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT);
478   }
479 
getTokenServerEncodedUrl(DefaultCredentialsProvider provider)480   public static String getTokenServerEncodedUrl(DefaultCredentialsProvider provider) {
481     return getMetadataServerUrl(provider)
482         + "/computeMetadata/v1/instance/service-accounts/default/token";
483   }
484 
getTokenServerEncodedUrl()485   public static String getTokenServerEncodedUrl() {
486     return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT);
487   }
488 
getUniverseDomainUrl()489   public static String getUniverseDomainUrl() {
490     return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
491         + "/computeMetadata/v1/universe/universe_domain";
492   }
493 
getServiceAccountsUrl()494   public static String getServiceAccountsUrl() {
495     return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
496         + "/computeMetadata/v1/instance/service-accounts/?recursive=true";
497   }
498 
getIdentityDocumentUrl()499   public static String getIdentityDocumentUrl() {
500     return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
501         + "/computeMetadata/v1/instance/service-accounts/default/identity";
502   }
503 
504   @Override
hashCode()505   public int hashCode() {
506     return Objects.hash(transportFactoryClassName);
507   }
508 
509   @Override
toStringHelper()510   protected ToStringHelper toStringHelper() {
511     synchronized (this) {
512       return super.toStringHelper()
513           .add("transportFactoryClassName", transportFactoryClassName)
514           .add("scopes", scopes)
515           .add("universeDomainFromMetadata", universeDomainFromMetadata);
516     }
517   }
518 
519   @Override
equals(Object obj)520   public boolean equals(Object obj) {
521     if (!(obj instanceof ComputeEngineCredentials)) {
522       return false;
523     }
524     if (!super.equals(obj)) {
525       return false;
526     }
527     ComputeEngineCredentials other = (ComputeEngineCredentials) obj;
528     return Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
529         && Objects.equals(this.scopes, other.scopes)
530         && Objects.equals(this.universeDomainFromMetadata, other.universeDomainFromMetadata);
531   }
532 
readObject(ObjectInputStream input)533   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
534     input.defaultReadObject();
535     transportFactory = newInstance(transportFactoryClassName);
536   }
537 
538   @Override
toBuilder()539   public Builder toBuilder() {
540     return new Builder(this);
541   }
542 
newBuilder()543   public static Builder newBuilder() {
544     return new Builder();
545   }
546 
547   /**
548    * Returns the email address associated with the GCE default service account.
549    *
550    * @throws RuntimeException if the default service account cannot be read
551    */
552   @Override
553   // todo(#314) getAccount should not throw a RuntimeException
getAccount()554   public String getAccount() {
555     if (serviceAccountEmail == null) {
556       try {
557         serviceAccountEmail = getDefaultServiceAccount();
558       } catch (IOException ex) {
559         throw new RuntimeException("Failed to get service account", ex);
560       }
561     }
562     return serviceAccountEmail;
563   }
564 
565   /**
566    * Signs the provided bytes using the private key associated with the service account.
567    *
568    * <p>The Compute Engine's project must enable the Identity and Access Management (IAM) API and
569    * the instance's service account must have the iam.serviceAccounts.signBlob permission.
570    *
571    * @param toSign bytes to sign
572    * @return signed bytes
573    * @throws SigningException if the attempt to sign the provided bytes failed
574    * @see <a
575    *     href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob
576    *     Signing</a>
577    */
578   @Override
sign(byte[] toSign)579   public byte[] sign(byte[] toSign) {
580     try {
581       String account = getAccount();
582       return IamUtils.sign(
583           account, this, transportFactory.create(), toSign, Collections.<String, Object>emptyMap());
584     } catch (SigningException ex) {
585       throw ex;
586     } catch (RuntimeException ex) {
587       throw new SigningException("Signing failed", ex);
588     }
589   }
590 
getDefaultServiceAccount()591   private String getDefaultServiceAccount() throws IOException {
592     HttpResponse response = getMetadataResponse(getServiceAccountsUrl());
593     int statusCode = response.getStatusCode();
594     if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
595       throw new IOException(
596           String.format(
597               "Error code %s trying to get service accounts from"
598                   + " Compute Engine metadata. This may be because the virtual machine instance"
599                   + " does not have permission scopes specified.",
600               statusCode));
601     }
602     if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
603       throw new IOException(
604           String.format(
605               "Unexpected Error code %s trying to get service accounts"
606                   + " from Compute Engine metadata: %s",
607               statusCode, response.parseAsString()));
608     }
609     InputStream content = response.getContent();
610     if (content == null) {
611       // Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
612       // Mock transports will have success code with empty content by default.
613       throw new IOException("Empty content from metadata token server request.");
614     }
615     GenericData responseData = response.parseAs(GenericData.class);
616     Map<String, Object> defaultAccount =
617         OAuth2Utils.validateMap(responseData, "default", PARSE_ERROR_ACCOUNT);
618     return OAuth2Utils.validateString(defaultAccount, "email", PARSE_ERROR_ACCOUNT);
619   }
620 
621   public static class Builder extends GoogleCredentials.Builder {
622     private HttpTransportFactory transportFactory;
623     private Collection<String> scopes;
624     private Collection<String> defaultScopes;
625 
Builder()626     protected Builder() {
627       setRefreshMargin(COMPUTE_REFRESH_MARGIN);
628       setExpirationMargin(COMPUTE_EXPIRATION_MARGIN);
629     }
630 
Builder(ComputeEngineCredentials credentials)631     protected Builder(ComputeEngineCredentials credentials) {
632       super(credentials);
633       this.transportFactory = credentials.transportFactory;
634       this.scopes = credentials.scopes;
635     }
636 
637     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)638     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
639       this.transportFactory = transportFactory;
640       return this;
641     }
642 
643     @CanIgnoreReturnValue
setScopes(Collection<String> scopes)644     public Builder setScopes(Collection<String> scopes) {
645       this.scopes = scopes;
646       return this;
647     }
648 
649     @CanIgnoreReturnValue
setDefaultScopes(Collection<String> defaultScopes)650     public Builder setDefaultScopes(Collection<String> defaultScopes) {
651       this.defaultScopes = defaultScopes;
652       return this;
653     }
654 
655     @CanIgnoreReturnValue
setUniverseDomain(String universeDomain)656     public Builder setUniverseDomain(String universeDomain) {
657       this.universeDomain = universeDomain;
658       return this;
659     }
660 
661     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)662     public Builder setQuotaProjectId(String quotaProjectId) {
663       super.quotaProjectId = quotaProjectId;
664       return this;
665     }
666 
getHttpTransportFactory()667     public HttpTransportFactory getHttpTransportFactory() {
668       return transportFactory;
669     }
670 
getScopes()671     public Collection<String> getScopes() {
672       return scopes;
673     }
674 
getDefaultScopes()675     public Collection<String> getDefaultScopes() {
676       return defaultScopes;
677     }
678 
679     @Override
build()680     public ComputeEngineCredentials build() {
681       return new ComputeEngineCredentials(this);
682     }
683   }
684 }
685