Skip to content

Commit 047de72

Browse files
Retry GCE environment check for Application Default Credentials (#110)
Adapt fix from Apiary client libraries. Addresses #109.
1 parent 1b07485 commit 047de72

File tree

5 files changed

+120
-31
lines changed

5 files changed

+120
-31
lines changed

google-auth-library-java/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ following are searched (in order) to find the Application Default Credentials:
8484
3. Google App Engine built-in credentials
8585
4. Google Cloud Shell built-in credentials
8686
5. Google Compute Engine built-in credentials
87+
- Skip this check by setting the environment variable `NO_GCE_CHECK=true`
88+
- Customize the GCE metadata server address by setting the environment variable `GCE_METADATA_HOST=<hostname>`
8789

8890
To get Credentials from a Service Account JSON key use `GoogleCredentials.fromStream(InputStream)`
8991
or `GoogleCredentials.fromStream(InputStream, HttpTransportFactory)`.

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
import com.google.api.client.util.GenericData;
4343
import com.google.auth.http.HttpTransportFactory;
4444
import com.google.common.base.MoreObjects;
45-
4645
import java.io.IOException;
4746
import java.io.InputStream;
4847
import java.io.ObjectInputStream;
48+
import java.net.SocketTimeoutException;
4949
import java.net.UnknownHostException;
5050
import java.util.Date;
5151
import java.util.Objects;
52+
import java.util.logging.Level;
53+
import java.util.logging.Logger;
5254

5355
/**
5456
* OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
@@ -57,9 +59,21 @@
5759
*/
5860
public class ComputeEngineCredentials extends GoogleCredentials {
5961

60-
static final String TOKEN_SERVER_ENCODED_URL =
61-
"http://metadata/computeMetadata/v1/instance/service-accounts/default/token";
62-
static final String METADATA_SERVER_URL = "http://metadata.google.internal";
62+
private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());
63+
64+
// Note: the explicit IP address is used to avoid name server resolution issues.
65+
static final String DEFAULT_METADATA_SERVER_URL = "http://169.254.169.254";
66+
67+
// Note: the explicit `timeout` and `tries` below is a workaround. The underlying
68+
// issue is that resolving an unknown host on some networks will take
69+
// 20-30 seconds; making this timeout short fixes the issue, but
70+
// could lead to false negatives in the event that we are on GCE, but
71+
// the metadata resolution was particularly slow. The latter case is
72+
// "unlikely" since the expected 4-nines time is about 0.5 seconds.
73+
// This allows us to limit the total ping maximum timeout to 1.5 seconds
74+
// for developer desktop scenarios.
75+
static final int MAX_COMPUTE_PING_TRIES = 3;
76+
static final int COMPUTE_PING_CONNECTION_TIMEOUT_MS = 500;
6377

6478
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
6579
private static final long serialVersionUID = -4113476462526554235L;
@@ -92,7 +106,7 @@ public ComputeEngineCredentials(HttpTransportFactory transportFactory) {
92106
*/
93107
@Override
94108
public AccessToken refreshAccessToken() throws IOException {
95-
GenericUrl tokenUrl = new GenericUrl(TOKEN_SERVER_ENCODED_URL);
109+
GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl());
96110
HttpRequest request =
97111
transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
98112
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
@@ -133,27 +147,59 @@ public AccessToken refreshAccessToken() throws IOException {
133147
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
134148
}
135149

136-
/**
137-
* Return whether code is running on Google Compute Engine.
138-
*/
139-
static boolean runningOnComputeEngine(HttpTransportFactory transportFactory) {
140-
try {
141-
GenericUrl tokenUrl = new GenericUrl(METADATA_SERVER_URL);
142-
HttpRequest request =
143-
transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
144-
HttpResponse response = request.execute();
145-
// Internet providers can return a generic response to all requests, so it is necessary
146-
// to check that metadata header is present also.
147-
HttpHeaders headers = response.getHeaders();
148-
if (OAuth2Utils.headersContainValue(headers, "Metadata-Flavor", "Google")) {
149-
return true;
150+
/** Return whether code is running on Google Compute Engine. */
151+
static boolean runningOnComputeEngine(
152+
HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
153+
// If the environment has requested that we do no GCE checks, return immediately.
154+
if (Boolean.parseBoolean(provider.getEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR))) {
155+
return false;
156+
}
157+
158+
GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider));
159+
for (int i = 1; i <= MAX_COMPUTE_PING_TRIES; ++i) {
160+
try {
161+
HttpRequest request =
162+
transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
163+
request.setConnectTimeout(COMPUTE_PING_CONNECTION_TIMEOUT_MS);
164+
HttpResponse response = request.execute();
165+
try {
166+
// Internet providers can return a generic response to all requests, so it is necessary
167+
// to check that metadata header is present also.
168+
HttpHeaders headers = response.getHeaders();
169+
return OAuth2Utils.headersContainValue(headers, "Metadata-Flavor", "Google");
170+
} finally {
171+
response.disconnect();
172+
}
173+
} catch (SocketTimeoutException expected) {
174+
// Ignore logging timeouts which is the expected failure mode in non GCE environments.
175+
} catch (IOException e) {
176+
LOGGER.log(
177+
Level.WARNING, "Failed to detect whether we are running on Google Compute Engine.", e);
150178
}
151-
} catch (IOException expected) {
152-
// ignore
153179
}
154180
return false;
155181
}
156182

183+
public static String getMetadataServerUrl(DefaultCredentialsProvider provider) {
184+
String metadataServerAddress = provider.getEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR);
185+
if (metadataServerAddress != null) {
186+
return "http://" + metadataServerAddress;
187+
}
188+
return DEFAULT_METADATA_SERVER_URL;
189+
}
190+
191+
public static String getMetadataServerUrl() {
192+
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT);
193+
}
194+
195+
public static String getTokenServerEncodedUrl(DefaultCredentialsProvider provider) {
196+
return getMetadataServerUrl(provider) + "/computeMetadata/v1/instance/service-accounts/default/token";
197+
}
198+
199+
public static String getTokenServerEncodedUrl() {
200+
return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT);
201+
}
202+
157203
@Override
158204
public int hashCode() {
159205
return Objects.hash(transportFactoryClassName);

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
**/
5454
class DefaultCredentialsProvider {
5555

56+
static final DefaultCredentialsProvider DEFAULT = new DefaultCredentialsProvider();
57+
5658
static final String CREDENTIAL_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS";
5759

5860
static final String WELL_KNOWN_CREDENTIALS_FILE = "application_default_credentials.json";
@@ -68,6 +70,9 @@ class DefaultCredentialsProvider {
6870

6971
static final String SKIP_APP_ENGINE_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS_SKIP_APP_ENGINE";
7072

73+
static final String NO_GCE_CHECK_ENV_VAR = "NO_GCE_CHECK";
74+
static final String GCE_METADATA_HOST_ENV_VAR = "GCE_METADATA_HOST";
75+
7176
// These variables should only be accessed inside a synchronized block
7277
private GoogleCredentials cachedCredentials = null;
7378
private boolean checkedAppEngine = false;
@@ -259,7 +264,7 @@ private final GoogleCredentials tryGetComputeCredentials(HttpTransportFactory tr
259264
return null;
260265
}
261266
boolean runningOnComputeEngine =
262-
ComputeEngineCredentials.runningOnComputeEngine(transportFactory);
267+
ComputeEngineCredentials.runningOnComputeEngine(transportFactory, this);
263268
checkedComputeEngine = true;
264269
if (runningOnComputeEngine) {
265270
return new ComputeEngineCredentials(transportFactory);

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@
4848
import com.google.auth.oauth2.ComputeEngineCredentialsTest.MockMetadataServerTransportFactory;
4949
import com.google.auth.oauth2.GoogleCredentialsTest.MockHttpTransportFactory;
5050
import com.google.auth.oauth2.GoogleCredentialsTest.MockTokenServerTransportFactory;
51-
52-
import org.junit.Test;
53-
import org.junit.runner.RunWith;
54-
import org.junit.runners.JUnit4;
55-
5651
import java.io.File;
5752
import java.io.FileNotFoundException;
5853
import java.io.IOException;
@@ -64,6 +59,9 @@
6459
import java.util.HashMap;
6560
import java.util.List;
6661
import java.util.Map;
62+
import org.junit.Test;
63+
import org.junit.runner.RunWith;
64+
import org.junit.runners.JUnit4;
6765

6866
/**
6967
* Test case for {@link DefaultCredentialsProvider}.
@@ -157,14 +155,18 @@ public void getDefaultCredentials_noCredentials_singleGceTestRequest() {
157155
} catch (IOException expected) {
158156
// Expected
159157
}
160-
assertEquals(1, transportFactory.transport.getRequestCount());
158+
assertEquals(
159+
transportFactory.transport.getRequestCount(),
160+
ComputeEngineCredentials.MAX_COMPUTE_PING_TRIES);
161161
try {
162162
testProvider.getDefaultCredentials(transportFactory);
163163
fail("No credential expected.");
164164
} catch (IOException expected) {
165165
// Expected
166166
}
167-
assertEquals(1, transportFactory.transport.getRequestCount());
167+
assertEquals(
168+
transportFactory.transport.getRequestCount(),
169+
ComputeEngineCredentials.MAX_COMPUTE_PING_TRIES);
168170
}
169171

170172
@Test
@@ -315,6 +317,40 @@ public void getDefaultCredentials_envUser_providesToken() throws IOException {
315317
testProvider, USER_CLIENT_ID, USER_CLIENT_SECRET, REFRESH_TOKEN);
316318
}
317319

320+
@Test
321+
public void getDefaultCredentials_envNoGceCheck_noGceRequest() throws IOException {
322+
MockRequestCountingTransportFactory transportFactory =
323+
new MockRequestCountingTransportFactory();
324+
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
325+
testProvider.setEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR, "true");
326+
327+
try {
328+
testProvider.getDefaultCredentials(transportFactory);
329+
fail("No credential expected.");
330+
} catch (IOException expected) {
331+
// Expected
332+
}
333+
assertEquals(transportFactory.transport.getRequestCount(), 0);
334+
}
335+
336+
@Test
337+
public void getDefaultCredentials_envGceMetadataHost_setsMetadataServerUrl() {
338+
String testUrl = "192.0.2.0";
339+
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
340+
testProvider.setEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR, testUrl);
341+
assertEquals(ComputeEngineCredentials.getMetadataServerUrl(testProvider), "http://" + testUrl);
342+
}
343+
344+
@Test
345+
public void getDefaultCredentials_envGceMetadataHost_setsTokenServerUrl() {
346+
String testUrl = "192.0.2.0";
347+
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
348+
testProvider.setEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR, testUrl);
349+
assertEquals(
350+
ComputeEngineCredentials.getTokenServerEncodedUrl(testProvider),
351+
"http://" + testUrl + "/computeMetadata/v1/instance/service-accounts/default/token");
352+
}
353+
318354
@Test
319355
public void getDefaultCredentials_wellKnownFileEnv_providesToken() throws IOException {
320356
File cloudConfigDir = getTempDirectory();

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void setTokenRequestStatusCode(Integer tokenRequestStatusCode) {
6363

6464
@Override
6565
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
66-
if (url.equals(ComputeEngineCredentials.TOKEN_SERVER_ENCODED_URL)) {
66+
if (url.equals(ComputeEngineCredentials.getTokenServerEncodedUrl())) {
6767

6868
return new MockLowLevelHttpRequest(url) {
6969
@Override
@@ -93,7 +93,7 @@ public LowLevelHttpResponse execute() throws IOException {
9393
.setContent(refreshText);
9494
}
9595
};
96-
} else if (url.equals(ComputeEngineCredentials.METADATA_SERVER_URL)) {
96+
} else if (url.equals(ComputeEngineCredentials.getMetadataServerUrl())) {
9797
return new MockLowLevelHttpRequest(url) {
9898
@Override
9999
public LowLevelHttpResponse execute() {

0 commit comments

Comments
 (0)