1 /* 2 * Copyright 2016 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.auth; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.auth.Credentials; 22 import com.google.auth.RequestMetadataCallback; 23 import com.google.common.annotations.VisibleForTesting; 24 import com.google.common.io.BaseEncoding; 25 import io.grpc.InternalMayRequireSpecificExecutor; 26 import io.grpc.Metadata; 27 import io.grpc.MethodDescriptor; 28 import io.grpc.SecurityLevel; 29 import io.grpc.Status; 30 import io.grpc.StatusException; 31 import java.io.IOException; 32 import java.lang.reflect.InvocationTargetException; 33 import java.lang.reflect.Method; 34 import java.net.URI; 35 import java.net.URISyntaxException; 36 import java.util.ArrayList; 37 import java.util.Collection; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.concurrent.Executor; 41 import java.util.logging.Level; 42 import java.util.logging.Logger; 43 import javax.annotation.Nullable; 44 45 /** 46 * Wraps {@link Credentials} as a {@link io.grpc.CallCredentials}. 47 */ 48 final class GoogleAuthLibraryCallCredentials extends io.grpc.CallCredentials 49 implements InternalMayRequireSpecificExecutor { 50 private static final Logger log 51 = Logger.getLogger(GoogleAuthLibraryCallCredentials.class.getName()); 52 private static final JwtHelper jwtHelper 53 = createJwtHelperOrNull(GoogleAuthLibraryCallCredentials.class.getClassLoader()); 54 private static final Class<? extends Credentials> GOOGLE_CREDENTIALS_CLASS 55 = loadGoogleCredentialsClass(); 56 private static final Class<?> APP_ENGINE_CREDENTIALS_CLASS 57 = loadAppEngineCredentials(); 58 59 private final boolean requirePrivacy; 60 @VisibleForTesting 61 final Credentials creds; 62 63 private Metadata lastHeaders; 64 private Map<String, List<String>> lastMetadata; 65 66 private final boolean requiresSpecificExecutor; 67 GoogleAuthLibraryCallCredentials(Credentials creds)68 public GoogleAuthLibraryCallCredentials(Credentials creds) { 69 this(creds, jwtHelper); 70 } 71 72 @VisibleForTesting GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper)73 GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper) { 74 checkNotNull(creds, "creds"); 75 boolean requirePrivacy = false; 76 if (GOOGLE_CREDENTIALS_CLASS != null) { 77 // All GoogleCredentials instances are bearer tokens and should only be used on private 78 // channels. This catches all return values from GoogleCredentials.getApplicationDefault(). 79 // This should be checked before upgrading the Service Account to JWT, as JWT is also a bearer 80 // token. 81 requirePrivacy = GOOGLE_CREDENTIALS_CLASS.isInstance(creds); 82 } 83 if (jwtHelper != null) { 84 creds = jwtHelper.tryServiceAccountToJwt(creds); 85 } 86 this.requirePrivacy = requirePrivacy; 87 this.creds = creds; 88 89 // Cache the value so we only need to try to load the class once 90 if (APP_ENGINE_CREDENTIALS_CLASS == null) { 91 requiresSpecificExecutor = false; 92 } else { 93 requiresSpecificExecutor = APP_ENGINE_CREDENTIALS_CLASS.isInstance(creds); 94 } 95 } 96 97 @Override applyRequestMetadata( RequestInfo info, Executor appExecutor, final MetadataApplier applier)98 public void applyRequestMetadata( 99 RequestInfo info, Executor appExecutor, final MetadataApplier applier) { 100 SecurityLevel security = info.getSecurityLevel(); 101 if (requirePrivacy && security != SecurityLevel.PRIVACY_AND_INTEGRITY) { 102 applier.fail(Status.UNAUTHENTICATED 103 .withDescription("Credentials require channel with PRIVACY_AND_INTEGRITY security level. " 104 + "Observed security level: " + security)); 105 return; 106 } 107 108 String authority = checkNotNull(info.getAuthority(), "authority"); 109 final URI uri; 110 try { 111 uri = serviceUri(authority, info.getMethodDescriptor()); 112 } catch (StatusException e) { 113 applier.fail(e.getStatus()); 114 return; 115 } 116 // Credentials is expected to manage caching internally if the metadata is fetched over 117 // the network. 118 creds.getRequestMetadata(uri, appExecutor, new RequestMetadataCallback() { 119 @Override 120 public void onSuccess(Map<String, List<String>> metadata) { 121 // Some implementations may pass null metadata. 122 123 // Re-use the headers if getRequestMetadata() returns the same map. It may return a 124 // different map based on the provided URI, i.e., for JWT. However, today it does not 125 // cache JWT and so we won't bother tring to save its return value based on the URI. 126 Metadata headers; 127 try { 128 synchronized (GoogleAuthLibraryCallCredentials.this) { 129 if (lastMetadata == null || lastMetadata != metadata) { 130 lastHeaders = toHeaders(metadata); 131 lastMetadata = metadata; 132 } 133 headers = lastHeaders; 134 } 135 } catch (Throwable t) { 136 applier.fail(Status.UNAUTHENTICATED 137 .withDescription("Failed to convert credential metadata") 138 .withCause(t)); 139 return; 140 } 141 applier.apply(headers); 142 } 143 144 @Override 145 public void onFailure(Throwable e) { 146 if (e instanceof IOException) { 147 // Since it's an I/O failure, let the call be retried with UNAVAILABLE. 148 applier.fail(Status.UNAVAILABLE 149 .withDescription("Credentials failed to obtain metadata") 150 .withCause(e)); 151 } else { 152 applier.fail(Status.UNAUTHENTICATED 153 .withDescription("Failed computing credential metadata") 154 .withCause(e)); 155 } 156 } 157 }); 158 } 159 160 /** 161 * Generate a JWT-specific service URI. The URI is simply an identifier with enough information 162 * for a service to know that the JWT was intended for it. The URI will commonly be verified with 163 * a simple string equality check. 164 */ serviceUri(String authority, MethodDescriptor<?, ?> method)165 private static URI serviceUri(String authority, MethodDescriptor<?, ?> method) 166 throws StatusException { 167 // Always use HTTPS, by definition. 168 final String scheme = "https"; 169 final int defaultPort = 443; 170 String path = "/" + method.getServiceName(); 171 URI uri; 172 try { 173 uri = new URI(scheme, authority, path, null, null); 174 } catch (URISyntaxException e) { 175 throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth") 176 .withCause(e).asException(); 177 } 178 // The default port must not be present. Alternative ports should be present. 179 if (uri.getPort() == defaultPort) { 180 uri = removePort(uri); 181 } 182 return uri; 183 } 184 removePort(URI uri)185 private static URI removePort(URI uri) throws StatusException { 186 try { 187 return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */, 188 uri.getPath(), uri.getQuery(), uri.getFragment()); 189 } catch (URISyntaxException e) { 190 throw Status.UNAUTHENTICATED.withDescription( 191 "Unable to construct service URI after removing port").withCause(e).asException(); 192 } 193 } 194 toHeaders(@ullable Map<String, List<String>> metadata)195 private static Metadata toHeaders(@Nullable Map<String, List<String>> metadata) { 196 Metadata headers = new Metadata(); 197 if (metadata != null) { 198 for (String key : metadata.keySet()) { 199 if (key.endsWith("-bin")) { 200 Metadata.Key<byte[]> headerKey = Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER); 201 for (String value : metadata.get(key)) { 202 headers.put(headerKey, BaseEncoding.base64().decode(value)); 203 } 204 } else { 205 Metadata.Key<String> headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); 206 for (String value : metadata.get(key)) { 207 headers.put(headerKey, value); 208 } 209 } 210 } 211 } 212 return headers; 213 } 214 215 @VisibleForTesting 216 @Nullable createJwtHelperOrNull(ClassLoader loader)217 static JwtHelper createJwtHelperOrNull(ClassLoader loader) { 218 Class<?> rawServiceAccountClass; 219 try { 220 // Specify loader so it can be overridden in tests 221 rawServiceAccountClass 222 = Class.forName("com.google.auth.oauth2.ServiceAccountCredentials", false, loader); 223 } catch (ClassNotFoundException ex) { 224 return null; 225 } 226 Exception caughtException; 227 try { 228 return new JwtHelper(rawServiceAccountClass, loader); 229 } catch (ClassNotFoundException ex) { 230 caughtException = ex; 231 } catch (NoSuchMethodException ex) { 232 caughtException = ex; 233 } 234 if (caughtException != null) { 235 // Failure is a bug in this class, but we still choose to gracefully recover 236 log.log(Level.WARNING, "Failed to create JWT helper. This is unexpected", caughtException); 237 } 238 return null; 239 } 240 241 @Nullable loadGoogleCredentialsClass()242 private static Class<? extends Credentials> loadGoogleCredentialsClass() { 243 Class<?> rawGoogleCredentialsClass; 244 try { 245 // Can't use a loader as it disables ProGuard's reference detection and would fail to rename 246 // this reference. Unfortunately this will initialize the class. 247 rawGoogleCredentialsClass = Class.forName("com.google.auth.oauth2.GoogleCredentials"); 248 } catch (ClassNotFoundException ex) { 249 log.log(Level.FINE, "Failed to load GoogleCredentials", ex); 250 return null; 251 } 252 return rawGoogleCredentialsClass.asSubclass(Credentials.class); 253 } 254 255 @Nullable loadAppEngineCredentials()256 private static Class<?> loadAppEngineCredentials() { 257 try { 258 return Class.forName("com.google.auth.appengine.AppEngineCredentials"); 259 } catch (ClassNotFoundException ex) { 260 log.log(Level.FINE, "AppEngineCredentials not available in classloader", ex); 261 return null; 262 } 263 } 264 265 private static class MethodPair { 266 private final Method getter; 267 private final Method builderSetter; 268 MethodPair(Method getter, Method builderSetter)269 private MethodPair(Method getter, Method builderSetter) { 270 this.getter = getter; 271 this.builderSetter = builderSetter; 272 } 273 apply(Credentials credentials, Object builder)274 private void apply(Credentials credentials, Object builder) 275 throws InvocationTargetException, IllegalAccessException { 276 builderSetter.invoke(builder, getter.invoke(credentials)); 277 } 278 } 279 280 @VisibleForTesting 281 static class JwtHelper { 282 private final Class<? extends Credentials> serviceAccountClass; 283 private final Method newJwtBuilder; 284 private final Method build; 285 private final Method getScopes; 286 private final List<MethodPair> methodPairs; 287 JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader)288 public JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader) 289 throws ClassNotFoundException, NoSuchMethodException { 290 serviceAccountClass = rawServiceAccountClass.asSubclass(Credentials.class); 291 getScopes = serviceAccountClass.getMethod("getScopes"); 292 Class<? extends Credentials> jwtClass = Class.forName( 293 "com.google.auth.oauth2.ServiceAccountJwtAccessCredentials", false, loader) 294 .asSubclass(Credentials.class); 295 newJwtBuilder = jwtClass.getDeclaredMethod("newBuilder"); 296 Class<?> builderClass = newJwtBuilder.getReturnType(); 297 build = builderClass.getMethod("build"); 298 299 methodPairs = new ArrayList<>(); 300 301 { 302 Method getter = serviceAccountClass.getMethod("getClientId"); 303 Method setter = builderClass.getMethod("setClientId", getter.getReturnType()); 304 methodPairs.add(new MethodPair(getter, setter)); 305 } 306 { 307 Method getter = serviceAccountClass.getMethod("getClientEmail"); 308 Method setter = builderClass.getMethod("setClientEmail", getter.getReturnType()); 309 methodPairs.add(new MethodPair(getter, setter)); 310 } 311 { 312 Method getter = serviceAccountClass.getMethod("getPrivateKey"); 313 Method setter = builderClass.getMethod("setPrivateKey", getter.getReturnType()); 314 methodPairs.add(new MethodPair(getter, setter)); 315 } 316 { 317 Method getter = serviceAccountClass.getMethod("getPrivateKeyId"); 318 Method setter = builderClass.getMethod("setPrivateKeyId", getter.getReturnType()); 319 methodPairs.add(new MethodPair(getter, setter)); 320 } 321 { 322 Method getter = serviceAccountClass.getMethod("getQuotaProjectId"); 323 Method setter = builderClass.getMethod("setQuotaProjectId", getter.getReturnType()); 324 methodPairs.add(new MethodPair(getter, setter)); 325 } 326 } 327 328 /** 329 * This method tries to convert a {@link Credentials} object to a 330 * ServiceAccountJwtAccessCredentials. The original credentials will be returned if: 331 * <ul> 332 * <li> The Credentials is not a ServiceAccountCredentials</li> 333 * <li> The ServiceAccountCredentials has scopes</li> 334 * <li> Something unexpected happens </li> 335 * </ul> 336 * @param creds the Credentials to convert 337 * @return either the original Credentials or a fully formed ServiceAccountJwtAccessCredentials. 338 */ tryServiceAccountToJwt(Credentials creds)339 public Credentials tryServiceAccountToJwt(Credentials creds) { 340 if (!serviceAccountClass.isInstance(creds)) { 341 return creds; 342 } 343 Exception caughtException; 344 try { 345 creds = serviceAccountClass.cast(creds); 346 Collection<?> scopes = (Collection<?>) getScopes.invoke(creds); 347 if (scopes.size() != 0) { 348 // Leave as-is, since the scopes may limit access within the service. 349 return creds; 350 } 351 // Create the JWT Credentials Builder 352 Object builder = newJwtBuilder.invoke(null); 353 354 // Get things from the credentials, and set them on the builder. 355 for (MethodPair pair : this.methodPairs) { 356 pair.apply(creds, builder); 357 } 358 359 // Call builder.build() 360 return (Credentials) build.invoke(builder); 361 } catch (IllegalAccessException ex) { 362 caughtException = ex; 363 } catch (InvocationTargetException ex) { 364 caughtException = ex; 365 } 366 if (caughtException != null) { 367 // Failure is a bug in this class, but we still choose to gracefully recover 368 log.log( 369 Level.WARNING, 370 "Failed converting service account credential to JWT. This is unexpected", 371 caughtException); 372 } 373 return creds; 374 } 375 } 376 377 /** 378 * This method is to support the hack for AppEngineCredentials which need to run on a 379 * specific thread. 380 * @return Whether a specific executor is needed or if any executor can be used 381 */ 382 @Override isSpecificExecutorRequired()383 public boolean isSpecificExecutorRequired() { 384 return requiresSpecificExecutor; 385 } 386 387 } 388