1 /* 2 * Copyright 2016, 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.ServiceAccountSigner; 35 import com.google.common.base.MoreObjects; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.ImmutableSet; 38 import java.io.IOException; 39 import java.io.ObjectInputStream; 40 import java.lang.reflect.InvocationTargetException; 41 import java.lang.reflect.Method; 42 import java.util.Collection; 43 import java.util.Date; 44 import java.util.Objects; 45 46 /** 47 * OAuth2 credentials representing the built-in service account for Google App Engine. 48 * 49 * <p>Instances of this class use reflection to access AppIdentityService in AppEngine SDK. 50 */ 51 class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSigner { 52 53 private static final long serialVersionUID = -493219027336622194L; 54 55 static final String APP_IDENTITY_SERVICE_FACTORY_CLASS = 56 "com.google.appengine.api.appidentity.AppIdentityServiceFactory"; 57 static final String APP_IDENTITY_SERVICE_CLASS = 58 "com.google.appengine.api.appidentity.AppIdentityService"; 59 static final String GET_ACCESS_TOKEN_RESULT_CLASS = 60 "com.google.appengine.api.appidentity.AppIdentityService$GetAccessTokenResult"; 61 static final String SIGNING_RESULT_CLASS = 62 "com.google.appengine.api.appidentity.AppIdentityService$SigningResult"; 63 private static final String GET_APP_IDENTITY_SERVICE_METHOD = "getAppIdentityService"; 64 private static final String GET_ACCESS_TOKEN_RESULT_METHOD = "getAccessToken"; 65 private static final String GET_ACCESS_TOKEN_METHOD = "getAccessToken"; 66 private static final String GET_EXPIRATION_TIME_METHOD = "getExpirationTime"; 67 private static final String GET_SERVICE_ACCOUNT_NAME_METHOD = "getServiceAccountName"; 68 private static final String SIGN_FOR_APP_METHOD = "signForApp"; 69 private static final String GET_SIGNATURE_METHOD = "getSignature"; 70 71 private final Collection<String> scopes; 72 private final boolean scopesRequired; 73 74 private transient Object appIdentityService; 75 private transient Method getAccessToken; 76 private transient Method getAccessTokenResult; 77 private transient Method getExpirationTime; 78 private transient Method signForApp; 79 private transient Method getSignature; 80 private transient String account; 81 AppEngineCredentials(Collection<String> scopes, Collection<String> defaultScopes)82 AppEngineCredentials(Collection<String> scopes, Collection<String> defaultScopes) 83 throws IOException { 84 // Use defaultScopes only when scopes don't exist. 85 if (scopes == null || scopes.isEmpty()) { 86 this.scopes = 87 defaultScopes == null ? ImmutableList.<String>of() : ImmutableList.copyOf(defaultScopes); 88 } else { 89 this.scopes = ImmutableList.copyOf(scopes); 90 } 91 this.scopesRequired = this.scopes.isEmpty(); 92 init(); 93 } 94 AppEngineCredentials( Collection<String> scopes, Collection<String> defaultScopes, AppEngineCredentials unscoped)95 AppEngineCredentials( 96 Collection<String> scopes, Collection<String> defaultScopes, AppEngineCredentials unscoped) { 97 this.appIdentityService = unscoped.appIdentityService; 98 this.getAccessToken = unscoped.getAccessToken; 99 this.getAccessTokenResult = unscoped.getAccessTokenResult; 100 this.getExpirationTime = unscoped.getExpirationTime; 101 // Use defaultScopes only when scopes don't exist. 102 if (scopes == null || scopes.isEmpty()) { 103 this.scopes = 104 defaultScopes == null ? ImmutableSet.<String>of() : ImmutableList.copyOf(defaultScopes); 105 } else { 106 this.scopes = ImmutableList.copyOf(scopes); 107 } 108 this.scopesRequired = this.scopes.isEmpty(); 109 } 110 init()111 private void init() throws IOException { 112 try { 113 Class<?> factoryClass = forName(APP_IDENTITY_SERVICE_FACTORY_CLASS); 114 Method method = factoryClass.getMethod(GET_APP_IDENTITY_SERVICE_METHOD); 115 this.appIdentityService = method.invoke(null); 116 Class<?> serviceClass = forName(APP_IDENTITY_SERVICE_CLASS); 117 Class<?> tokenResultClass = forName(GET_ACCESS_TOKEN_RESULT_CLASS); 118 this.getAccessTokenResult = 119 serviceClass.getMethod(GET_ACCESS_TOKEN_RESULT_METHOD, Iterable.class); 120 this.getAccessToken = tokenResultClass.getMethod(GET_ACCESS_TOKEN_METHOD); 121 this.getExpirationTime = tokenResultClass.getMethod(GET_EXPIRATION_TIME_METHOD); 122 this.account = 123 (String) 124 serviceClass.getMethod(GET_SERVICE_ACCOUNT_NAME_METHOD).invoke(appIdentityService); 125 this.signForApp = serviceClass.getMethod(SIGN_FOR_APP_METHOD, byte[].class); 126 Class<?> signingResultClass = forName(SIGNING_RESULT_CLASS); 127 this.getSignature = signingResultClass.getMethod(GET_SIGNATURE_METHOD); 128 } catch (ClassNotFoundException 129 | NoSuchMethodException 130 | IllegalAccessException 131 | InvocationTargetException ex) { 132 throw new IOException( 133 "Application Default Credentials failed to create the Google App Engine service account" 134 + " credentials. Check that the App Engine SDK is deployed.", 135 ex); 136 } 137 } 138 139 /** Refresh the access token by getting it from the App Identity service. */ 140 @Override refreshAccessToken()141 public AccessToken refreshAccessToken() throws IOException { 142 if (createScopedRequired()) { 143 throw new IOException("AppEngineCredentials requires createScoped call before use."); 144 } 145 try { 146 Object accessTokenResult = getAccessTokenResult.invoke(appIdentityService, scopes); 147 String accessToken = (String) getAccessToken.invoke(accessTokenResult); 148 Date expirationTime = (Date) getExpirationTime.invoke(accessTokenResult); 149 return new AccessToken(accessToken, expirationTime); 150 } catch (Exception e) { 151 throw new IOException("Could not get the access token.", e); 152 } 153 } 154 155 @Override createScopedRequired()156 public boolean createScopedRequired() { 157 return scopesRequired; 158 } 159 160 @Override createScoped(Collection<String> scopes)161 public GoogleCredentials createScoped(Collection<String> scopes) { 162 return new AppEngineCredentials(scopes, null, this); 163 } 164 165 @Override createScoped( Collection<String> scopes, Collection<String> defaultScopes)166 public GoogleCredentials createScoped( 167 Collection<String> scopes, Collection<String> defaultScopes) { 168 return new AppEngineCredentials(scopes, defaultScopes, this); 169 } 170 171 @Override getAccount()172 public String getAccount() { 173 return account; 174 } 175 176 @Override sign(byte[] toSign)177 public byte[] sign(byte[] toSign) { 178 try { 179 Object signingResult = signForApp.invoke(appIdentityService, toSign); 180 return (byte[]) getSignature.invoke(signingResult); 181 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { 182 throw new SigningException("Failed to sign the provided bytes", ex); 183 } 184 } 185 186 @Override hashCode()187 public int hashCode() { 188 return Objects.hash(scopes, scopesRequired); 189 } 190 191 @Override toString()192 public String toString() { 193 return MoreObjects.toStringHelper(this) 194 .add("scopes", scopes) 195 .add("scopesRequired", scopesRequired) 196 .toString(); 197 } 198 199 @Override equals(Object obj)200 public boolean equals(Object obj) { 201 if (!(obj instanceof AppEngineCredentials)) { 202 return false; 203 } 204 AppEngineCredentials other = (AppEngineCredentials) obj; 205 return this.scopesRequired == other.scopesRequired && Objects.equals(this.scopes, other.scopes); 206 } 207 readObject(ObjectInputStream input)208 private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 209 input.defaultReadObject(); 210 init(); 211 } 212 213 /* 214 * Start of methods to allow overriding in the test code to isolate from the environment. 215 */ 216 forName(String className)217 Class<?> forName(String className) throws ClassNotFoundException { 218 return Class.forName(className); 219 } 220 221 /* 222 * End of methods to allow overriding in the test code to isolate from the environment. 223 */ 224 } 225