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 com.google.auth.http.HttpTransportFactory; 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileNotFoundException; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.lang.reflect.Field; 41 import java.lang.reflect.InvocationTargetException; 42 import java.lang.reflect.Method; 43 import java.security.AccessControlException; 44 import java.util.Collections; 45 import java.util.Locale; 46 import java.util.logging.Level; 47 import java.util.logging.Logger; 48 49 /** 50 * Provides the Application Default Credential from the environment. 51 * 52 * <p>An instance represents the per-process state used to get and cache the credential and allows 53 * overriding the state and environment for testing purposes. 54 */ 55 class DefaultCredentialsProvider { 56 static final DefaultCredentialsProvider DEFAULT = new DefaultCredentialsProvider(); 57 static final String CREDENTIAL_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS"; 58 static final String QUOTA_PROJECT_ENV_VAR = "GOOGLE_CLOUD_QUOTA_PROJECT"; 59 60 static final String WELL_KNOWN_CREDENTIALS_FILE = "application_default_credentials.json"; 61 static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; 62 static final String APP_ENGINE_SIGNAL_CLASS = "com.google.appengine.api.utils.SystemProperty"; 63 static final String CLOUD_SHELL_ENV_VAR = "DEVSHELL_CLIENT_PORT"; 64 static final String SKIP_APP_ENGINE_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS_SKIP_APP_ENGINE"; 65 static final String SPECIFICATION_VERSION = System.getProperty("java.specification.version"); 66 static final String GAE_RUNTIME_VERSION = 67 System.getProperty("com.google.appengine.runtime.version"); 68 static final String RUNTIME_JETTY_LOGGER = System.getProperty("org.eclipse.jetty.util.log.class"); 69 static final Logger LOGGER = Logger.getLogger(DefaultCredentialsProvider.class.getName()); 70 static final String NO_GCE_CHECK_ENV_VAR = "NO_GCE_CHECK"; 71 static final String GCE_METADATA_HOST_ENV_VAR = "GCE_METADATA_HOST"; 72 static final String CLOUDSDK_CLIENT_ID = 73 "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"; 74 static final String CLOUDSDK_CREDENTIALS_WARNING = 75 "You are authenticating using user credentials. " 76 + "For production, we recommend using service account credentials.\n\n" 77 + "To learn more about service account credentials, see " 78 + "http://cloud.google.com/docs/authentication/external/set-up-adc-on-cloud"; 79 80 static final String CLOUDSDK_MISSING_CREDENTIALS = 81 "Your default credentials were not found. To set up Application Default Credentials " 82 + "for your environment, see " 83 + "https://cloud.google.com/docs/authentication/external/set-up-adc."; 84 public static final String SUPPRESS_GCLOUD_CREDS_WARNING_ENV_VAR = 85 "SUPPRESS_GCLOUD_CREDS_WARNING"; 86 87 // These variables should only be accessed inside a synchronized block 88 private GoogleCredentials cachedCredentials = null; 89 private boolean checkedAppEngine = false; 90 private boolean checkedComputeEngine = false; 91 DefaultCredentialsProvider()92 DefaultCredentialsProvider() {} 93 94 /** 95 * Returns the Application Default Credentials. 96 * 97 * <p>Returns the Application Default Credentials which are used to identify and authorize the 98 * whole application. The following are searched (in order) to find the Application Default 99 * Credentials: 100 * 101 * <ol> 102 * <li>Credentials file pointed to by the {@code GOOGLE_APPLICATION_CREDENTIALS} environment 103 * variable 104 * <li>Credentials provided by the Google Cloud SDK {@code gcloud auth application-default 105 * login} command 106 * <li>Google App Engine built-in credentials 107 * <li>Google Cloud Shell built-in credentials 108 * <li>Google Compute Engine built-in credentials 109 * </ol> 110 * 111 * @param transportFactory HTTP transport factory, creates the transport used to get access 112 * tokens. 113 * @return the credentials instance. 114 * @throws IOException if the credentials cannot be created in the current environment. 115 */ getDefaultCredentials(HttpTransportFactory transportFactory)116 final GoogleCredentials getDefaultCredentials(HttpTransportFactory transportFactory) 117 throws IOException { 118 synchronized (this) { 119 if (cachedCredentials == null) { 120 cachedCredentials = getDefaultCredentialsUnsynchronized(transportFactory); 121 } 122 if (cachedCredentials != null) { 123 return cachedCredentials; 124 } 125 } 126 127 throw new IOException(CLOUDSDK_MISSING_CREDENTIALS); 128 } 129 getDefaultCredentialsUnsynchronized( HttpTransportFactory transportFactory)130 private final GoogleCredentials getDefaultCredentialsUnsynchronized( 131 HttpTransportFactory transportFactory) throws IOException { 132 133 // First try the environment variable 134 GoogleCredentials credentials = null; 135 String credentialsPath = getEnv(CREDENTIAL_ENV_VAR); 136 if (credentialsPath != null && credentialsPath.length() > 0) { 137 LOGGER.log( 138 Level.FINE, 139 String.format("Attempting to load credentials from file: %s", credentialsPath)); 140 InputStream credentialsStream = null; 141 try { 142 File credentialsFile = new File(credentialsPath); 143 if (!isFile(credentialsFile)) { 144 // Path will be put in the message from the catch block below 145 throw new IOException("File does not exist."); 146 } 147 credentialsStream = readStream(credentialsFile); 148 credentials = GoogleCredentials.fromStream(credentialsStream, transportFactory); 149 } catch (IOException e) { 150 // Although it is also the cause, the message of the caught exception can have very 151 // important information for diagnosing errors, so include its message in the 152 // outer exception message also. 153 throw new IOException( 154 String.format( 155 "Error reading credential file from environment variable %s, value '%s': %s", 156 CREDENTIAL_ENV_VAR, credentialsPath, e.getMessage()), 157 e); 158 } catch (AccessControlException expected) { 159 // Exception querying file system is expected on App-Engine 160 } finally { 161 if (credentialsStream != null) { 162 credentialsStream.close(); 163 } 164 } 165 } 166 167 // Then try the well-known file 168 if (credentials == null) { 169 File wellKnownFileLocation = getWellKnownCredentialsFile(); 170 InputStream credentialsStream = null; 171 try { 172 if (isFile(wellKnownFileLocation)) { 173 LOGGER.log( 174 Level.FINE, 175 String.format( 176 "Attempting to load credentials from well known file: %s", 177 wellKnownFileLocation.getCanonicalPath())); 178 credentialsStream = readStream(wellKnownFileLocation); 179 credentials = GoogleCredentials.fromStream(credentialsStream, transportFactory); 180 } 181 } catch (IOException e) { 182 throw new IOException( 183 String.format( 184 "Error reading credential file from location %s: %s", 185 wellKnownFileLocation, e.getMessage())); 186 } catch (AccessControlException expected) { 187 // Exception querying file system is expected on App-Engine 188 } finally { 189 if (credentialsStream != null) { 190 credentialsStream.close(); 191 } 192 } 193 warnAboutProblematicCredentials(credentials); 194 } 195 196 // Then try GAE 7 standard environment 197 if (credentials == null && isOnGAEStandard7() && !skipAppEngineCredentialsCheck()) { 198 LOGGER.log(Level.FINE, "Attempting to load credentials from GAE 7 Standard"); 199 credentials = tryGetAppEngineCredential(); 200 } 201 202 // Then try Cloud Shell. This must be done BEFORE checking 203 // Compute Engine, as Cloud Shell runs on GCE VMs. 204 if (credentials == null) { 205 LOGGER.log(Level.FINE, "Attempting to load credentials from Cloud Shell"); 206 credentials = tryGetCloudShellCredentials(); 207 } 208 209 // Then try Compute Engine and GAE 8 standard environment 210 if (credentials == null) { 211 LOGGER.log(Level.FINE, "Attempting to load credentials from GCE"); 212 credentials = tryGetComputeCredentials(transportFactory); 213 } 214 215 if (credentials != null) { 216 String quotaFromEnv = getEnv(QUOTA_PROJECT_ENV_VAR); 217 218 if (quotaFromEnv != null && quotaFromEnv.trim().length() > 0) { 219 credentials = credentials.createWithQuotaProject(quotaFromEnv); 220 } 221 } 222 223 return credentials; 224 } 225 getWellKnownCredentialsFile()226 private final File getWellKnownCredentialsFile() { 227 return GoogleAuthUtils.getWellKnownCredentialsFile(this); 228 } 229 warnAboutProblematicCredentials(GoogleCredentials credentials)230 private void warnAboutProblematicCredentials(GoogleCredentials credentials) { 231 if (credentials instanceof UserCredentials 232 && !Boolean.parseBoolean(getEnv(SUPPRESS_GCLOUD_CREDS_WARNING_ENV_VAR)) 233 && ComputeEngineCredentials.checkStaticGceDetection(this)) { 234 LOGGER.log(Level.WARNING, CLOUDSDK_CREDENTIALS_WARNING); 235 } 236 } 237 runningOnAppEngine()238 private boolean runningOnAppEngine() { 239 Class<?> systemPropertyClass = null; 240 try { 241 systemPropertyClass = forName(APP_ENGINE_SIGNAL_CLASS); 242 } catch (ClassNotFoundException expected) { 243 // SystemProperty will always be present on App Engine. 244 return false; 245 } 246 Exception cause; 247 Field environmentField; 248 try { 249 environmentField = systemPropertyClass.getField("environment"); 250 Object environmentValue = environmentField.get(null); 251 Class<?> environmentType = environmentField.getType(); 252 Method valueMethod = environmentType.getMethod("value"); 253 Object environmentValueValue = valueMethod.invoke(environmentValue); 254 return (environmentValueValue != null); 255 } catch (NoSuchFieldException 256 | SecurityException 257 | IllegalArgumentException 258 | IllegalAccessException 259 | NoSuchMethodException 260 | InvocationTargetException exception) { 261 cause = exception; 262 } 263 throw new RuntimeException( 264 String.format( 265 "Unexpected error trying to determine if runnning on Google App Engine: %s", 266 cause.getMessage()), 267 cause); 268 } 269 tryGetCloudShellCredentials()270 private GoogleCredentials tryGetCloudShellCredentials() { 271 String port = getEnv(CLOUD_SHELL_ENV_VAR); 272 if (port != null) { 273 return CloudShellCredentials.create(Integer.parseInt(port)); 274 } else { 275 return null; 276 } 277 } 278 tryGetAppEngineCredential()279 private GoogleCredentials tryGetAppEngineCredential() throws IOException { 280 // Checking for App Engine requires a class load, so check only once 281 if (checkedAppEngine) { 282 return null; 283 } 284 boolean onAppEngine = runningOnAppEngine(); 285 checkedAppEngine = true; 286 if (!onAppEngine) { 287 return null; 288 } 289 return new AppEngineCredentials( 290 Collections.<String>emptyList(), Collections.<String>emptyList()); 291 } 292 tryGetComputeCredentials(HttpTransportFactory transportFactory)293 private final GoogleCredentials tryGetComputeCredentials(HttpTransportFactory transportFactory) { 294 // Checking compute engine requires a round-trip, so check only once 295 if (checkedComputeEngine) { 296 return null; 297 } 298 boolean runningOnComputeEngine = ComputeEngineCredentials.isOnGce(transportFactory, this); 299 checkedComputeEngine = true; 300 if (runningOnComputeEngine) { 301 return ComputeEngineCredentials.newBuilder() 302 .setHttpTransportFactory(transportFactory) 303 .build(); 304 } 305 return null; 306 } 307 308 // Skip app engine check if environment variable 309 // GOOGLE_APPLICATION_CREDENTIALS_SKIP_APP_ENGINE = 1 or true skipAppEngineCredentialsCheck()310 private boolean skipAppEngineCredentialsCheck() { 311 boolean skip = false; // do not skip by default 312 String value = getEnv(SKIP_APP_ENGINE_ENV_VAR); 313 if (value != null) { 314 skip = value.equalsIgnoreCase("true") || value.equals("1"); 315 } 316 return skip; 317 } 318 isOnGAEStandard7()319 protected boolean isOnGAEStandard7() { 320 return GAE_RUNTIME_VERSION != null 321 && (SPECIFICATION_VERSION.equals("1.7") || RUNTIME_JETTY_LOGGER == null); 322 } 323 getOsName()324 String getOsName() { 325 return getProperty("os.name", "").toLowerCase(Locale.US); 326 } 327 328 /* 329 * Start of methods to allow overriding in the test code to isolate from the environment. 330 */ 331 forName(String className)332 Class<?> forName(String className) throws ClassNotFoundException { 333 return Class.forName(className); 334 } 335 getEnv(String name)336 String getEnv(String name) { 337 return System.getenv(name); 338 } 339 getProperty(String property, String def)340 String getProperty(String property, String def) { 341 return System.getProperty(property, def); 342 } 343 isFile(File file)344 boolean isFile(File file) { 345 return file.isFile(); 346 } 347 readStream(File file)348 InputStream readStream(File file) throws FileNotFoundException { 349 return new FileInputStream(file); 350 } 351 352 /* 353 * End of methods to allow overriding in the test code to isolate from the environment. 354 */ 355 } 356