/*
* Copyright (c) 2014 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.client.googleapis.auth.oauth2;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.Beta;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessControlException;
import java.util.Locale;
/**
* {@link Beta} <br/>
* Provides a default credential available from the host or from an environment variable.
*
* <p>An instance represents the per-process state used to get and cache the credential and
* allows overriding the state and environment for testing purposes.
*/
@Beta
class DefaultCredentialProvider {
static final String CREDENTIAL_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS";
static final String WELL_KNOWN_CREDENTIALS_FILE = "application_default_credentials.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
static final String HELP_PERMALINK =
"https://developers.google.com/accounts/docs/application-default-credentials";
static final String APP_ENGINE_CREDENTIAL_CLASS =
"com.google.api.client.googleapis.extensions.appengine.auth.oauth2"
+ ".AppIdentityCredential$AppEngineCredentialWrapper";
// These variables should only be accessed inside a synchronized block
private GoogleCredential cachedCredential = null;
private boolean checkedAppEngine = false;
private boolean checkedComputeEngine = false;
DefaultCredentialProvider() {}
/**
* {@link Beta} <br/>
* Returns the Application Default Credentials.
*
* <p>Returns the Application Default Credentials which are credentials that identify and
* authorize the whole application. This is the built-in service account if running on Google
* Compute Engine or the credentials file from the path in the environment variable
* GOOGLE_APPLICATION_CREDENTIALS.</p>
*
* @param transport the transport for Http calls.
* @param jsonFactory the factory for Json parsing and formatting.
* @return the credential instance.
* @throws IOException if the credential cannot be created in the current environment.
*/
final GoogleCredential getDefaultCredential(HttpTransport transport, JsonFactory jsonFactory)
throws IOException {
synchronized (this) {
if (cachedCredential == null) {
cachedCredential = getDefaultCredentialUnsynchronized(transport, jsonFactory);
}
if (cachedCredential != null) {
return cachedCredential;
}
}
throw new IOException(String.format(
"The Application Default Credentials are not available. They are available if running"
+ " in Google Compute Engine. Otherwise, the environment variable %s must be defined"
+ " pointing to a file defining the credentials. See %s for more information.",
CREDENTIAL_ENV_VAR,
HELP_PERMALINK));
}
private final GoogleCredential getDefaultCredentialUnsynchronized(
HttpTransport transport, JsonFactory jsonFactory) throws IOException {
// First try the environment variable
GoogleCredential credential = null;
String credentialsPath = getEnv(CREDENTIAL_ENV_VAR);
if (credentialsPath != null && credentialsPath.length() > 0) {
InputStream credentialsStream = null;
try {
File credentialsFile = new File(credentialsPath);
if (!credentialsFile.exists() || credentialsFile.isDirectory()) {
// Path will get in the message from the catch block below
throw new IOException("File does not exist.");
}
credentialsStream = new FileInputStream(credentialsFile);
credential = GoogleCredential.fromStream(credentialsStream, transport, jsonFactory);
} catch (IOException e) {
// Although it is also the cause, the message of the caught exception can have very
// important information for diagnosing errors, so include its message in the
// outer exception message also
throw OAuth2Utils.exceptionWithCause(new IOException(String.format(
"Error reading credential file from environment variable %s, value '%s': %s",
CREDENTIAL_ENV_VAR, credentialsPath, e.getMessage())), e);
} catch (AccessControlException expected) {
// Exception querying file system is expected on App-Engine
} finally {
if (credentialsStream != null) {
credentialsStream.close();
}
}
}
// Then try the well-known file
if (credential == null) {
File wellKnownFileLocation = getWellKnownCredentialsFile();
try {
if (fileExists(wellKnownFileLocation)) {
InputStream credentialsStream = null;
try {
credentialsStream = new FileInputStream(wellKnownFileLocation);
credential = GoogleCredential.fromStream(credentialsStream, transport, jsonFactory);
} catch (IOException e) {
throw new IOException(String.format(
"Error reading credential file from location %s: %s",
wellKnownFileLocation, e.getMessage()));
} finally {
if (credentialsStream != null) {
credentialsStream.close();
}
}
}
} catch (AccessControlException expected) {
// Exception querying file system is expected on App-Engine
}
}
// Then try App Engine
if (credential == null) {
credential = tryGetAppEngineCredential(transport, jsonFactory);
}
// Then try Compute Engine
if (credential == null) {
credential = tryGetComputeCredential(transport, jsonFactory);
}
return credential;
}
private final File getWellKnownCredentialsFile() {
File cloudConfigPath = null;
String os = getProperty("os.name", "").toLowerCase(Locale.US);
if (os.indexOf("windows") >= 0) {
File appDataPath = new File(getEnv("APPDATA"));
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
} else {
File configPath = new File(getProperty("user.home", ""), ".config");
cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
}
File credentialFilePath = new File(cloudConfigPath, WELL_KNOWN_CREDENTIALS_FILE);
return credentialFilePath;
}
/**
* Override in test code to isolate from environment.
*/
String getEnv(String name) {
return System.getenv(name);
}
/**
* Override in test code to isolate from environment.
*/
boolean fileExists(File file) {
return file.exists() && !file.isDirectory();
}
/**
* Override in test code to isolate from environment.
*/
String getProperty(String property, String def) {
return System.getProperty(property, def);
}
/**
* Override in test code to isolate from environment.
*/
Class<?> forName(String className) throws ClassNotFoundException {
return Class.forName(className);
}
private boolean runningOnAppEngine() {
Class<?> systemPropertyClass = null;
try {
systemPropertyClass = forName("com.google.appengine.api.utils.SystemProperty");
} catch (ClassNotFoundException expected) {
// SystemProperty will always be present on App Engine.
return false;
}
Exception cause = null;
Field environmentField;
try {
environmentField = systemPropertyClass.getField("environment");
Object environmentValue = environmentField.get(null);
Class<?> environmentType = environmentField.getType();
Method valueMethod = environmentType.getMethod("value");
Object environmentValueValue = valueMethod.invoke(environmentValue);
return (environmentValueValue != null);
} catch (NoSuchFieldException exception) {
cause = exception;
} catch (SecurityException exception) {
cause = exception;
} catch (IllegalArgumentException exception) {
cause = exception;
} catch (IllegalAccessException exception) {
cause = exception;
} catch (NoSuchMethodException exception) {
cause = exception;
} catch (InvocationTargetException exception) {
cause = exception;
}
throw OAuth2Utils.exceptionWithCause(new RuntimeException(String.format(
"Unexpcted error trying to determine if runnning on Google App Engine: %s",
cause.getMessage())), cause);
}
private final GoogleCredential tryGetAppEngineCredential(
HttpTransport transport, JsonFactory jsonFactory) throws IOException {
// Checking for App Engine requires a class load, so check only once
if (checkedAppEngine) {
return null;
}
boolean onAppEngine = runningOnAppEngine();
checkedAppEngine = true;
if (!onAppEngine) {
return null;
}
Exception innerException = null;
try {
Class<?> credentialClass = forName(APP_ENGINE_CREDENTIAL_CLASS);
Constructor<?> constructor = credentialClass
.getConstructor(HttpTransport.class, JsonFactory.class);
return (GoogleCredential) constructor.newInstance(transport, jsonFactory);
} catch (ClassNotFoundException e) {
innerException = e;
} catch (NoSuchMethodException e) {
innerException = e;
} catch (InstantiationException e) {
innerException = e;
} catch (IllegalAccessException e) {
innerException = e;
} catch (InvocationTargetException e) {
innerException = e;
}
throw OAuth2Utils.exceptionWithCause(new IOException(String.format(
"Application Default Credentials failed to create the Google App Engine service account"
+ " credentials class %s. Check that the component 'google-api-client-appengine' is"
+ " deployed.",
APP_ENGINE_CREDENTIAL_CLASS)), innerException);
}
private final GoogleCredential tryGetComputeCredential(
HttpTransport transport, JsonFactory jsonFactory) {
// Checking compute engine requires a round-trip, so check only once
if (checkedComputeEngine) {
return null;
}
boolean runningOnComputeEngine = OAuth2Utils.runningOnComputeEngine(transport);
checkedComputeEngine = true;
if (runningOnComputeEngine) {
return new ComputeGoogleCredential(transport, jsonFactory);
}
return null;
}
private static class ComputeGoogleCredential extends GoogleCredential {
/** Metadata Service Account token server encoded URL. */
private static final String TOKEN_SERVER_ENCODED_URL =
"http://metadata/computeMetadata/v1/instance/service-accounts/default/token";
ComputeGoogleCredential(HttpTransport transport, JsonFactory jsonFactory) {
super(new GoogleCredential.Builder()
.setTransport(transport)
.setJsonFactory(jsonFactory)
.setTokenServerEncodedUrl(TOKEN_SERVER_ENCODED_URL));
}
@Override
protected TokenResponse executeRefreshToken() throws IOException {
GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl());
HttpRequest request = getTransport().createRequestFactory().buildGetRequest(tokenUrl);
JsonObjectParser parser = new JsonObjectParser(getJsonFactory());
request.setParser(parser);
request.getHeaders().set("X-Google-Metadata-Request", true);
request.setThrowExceptionOnExecuteError(false);
HttpResponse response = request.execute();
int statusCode = response.getStatusCode();
if (statusCode == HttpStatusCodes.STATUS_CODE_OK) {
InputStream content = response.getContent();
if (content == null) {
// Throw explicitly rather than allow a later null reference as default mock
// transports return success codes with empty contents.
throw new IOException("Empty content from metadata token server request.");
}
return parser.parseAndClose(content, response.getContentCharset(), TokenResponse.class);
}
if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
throw new IOException(String.format("Error code %s trying to get security access token from"
+ " Compute Engine metadata for the default service account. This may be because"
+ " the virtual machine instance does not have permission scopes specified.",
statusCode));
}
throw new IOException(String.format("Unexpected Error code %s trying to get security access"
+ " token from Compute Engine metadata for the default service account: %s", statusCode,
response.parseAsString()));
}
}
}