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.Attributes; 26 import io.grpc.CallCredentials; 27 import io.grpc.Metadata; 28 import io.grpc.MethodDescriptor; 29 import io.grpc.SecurityLevel; 30 import io.grpc.Status; 31 import io.grpc.StatusException; 32 import java.io.IOException; 33 import java.lang.reflect.Constructor; 34 import java.lang.reflect.InvocationTargetException; 35 import java.lang.reflect.Method; 36 import java.net.URI; 37 import java.net.URISyntaxException; 38 import java.security.PrivateKey; 39 import java.util.Collection; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.concurrent.Executor; 43 import java.util.logging.Level; 44 import java.util.logging.Logger; 45 import javax.annotation.Nullable; 46 47 /** 48 * Wraps {@link Credentials} as a {@link CallCredentials}. 49 */ 50 final class GoogleAuthLibraryCallCredentials implements CallCredentials { 51 private static final Logger log 52 = Logger.getLogger(GoogleAuthLibraryCallCredentials.class.getName()); 53 private static final JwtHelper jwtHelper 54 = createJwtHelperOrNull(GoogleAuthLibraryCallCredentials.class.getClassLoader()); 55 private static final Class<? extends Credentials> googleCredentialsClass 56 = loadGoogleCredentialsClass(); 57 58 private final boolean requirePrivacy; 59 @VisibleForTesting 60 final Credentials creds; 61 62 private Metadata lastHeaders; 63 private Map<String, List<String>> lastMetadata; 64 GoogleAuthLibraryCallCredentials(Credentials creds)65 public GoogleAuthLibraryCallCredentials(Credentials creds) { 66 this(creds, jwtHelper); 67 } 68 69 @VisibleForTesting GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper)70 GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper) { 71 checkNotNull(creds, "creds"); 72 boolean requirePrivacy = false; 73 if (googleCredentialsClass != null) { 74 // All GoogleCredentials instances are bearer tokens and should only be used on private 75 // channels. This catches all return values from GoogleCredentials.getApplicationDefault(). 76 // This should be checked before upgrading the Service Account to JWT, as JWT is also a bearer 77 // token. 78 requirePrivacy = googleCredentialsClass.isInstance(creds); 79 } 80 if (jwtHelper != null) { 81 creds = jwtHelper.tryServiceAccountToJwt(creds); 82 } 83 this.requirePrivacy = requirePrivacy; 84 this.creds = creds; 85 } 86 87 @Override thisUsesUnstableApi()88 public void thisUsesUnstableApi() {} 89 90 @Override applyRequestMetadata(MethodDescriptor<?, ?> method, Attributes attrs, Executor appExecutor, final MetadataApplier applier)91 public void applyRequestMetadata(MethodDescriptor<?, ?> method, Attributes attrs, 92 Executor appExecutor, final MetadataApplier applier) { 93 SecurityLevel security = attrs.get(ATTR_SECURITY_LEVEL); 94 if (security == null) { 95 // Although the API says ATTR_SECURITY_LEVEL is required, no one was really looking at it thus 96 // there may be transports that got away without setting it. Now we start to check it, it'd 97 // be less disruptive to tolerate nulls. 98 security = SecurityLevel.NONE; 99 } 100 if (requirePrivacy && security != SecurityLevel.PRIVACY_AND_INTEGRITY) { 101 applier.fail(Status.UNAUTHENTICATED 102 .withDescription("Credentials require channel with PRIVACY_AND_INTEGRITY security level. " 103 + "Observed security level: " + security)); 104 return; 105 } 106 107 String authority = checkNotNull(attrs.get(ATTR_AUTHORITY), "authority"); 108 final URI uri; 109 try { 110 uri = serviceUri(authority, method); 111 } catch (StatusException e) { 112 applier.fail(e.getStatus()); 113 return; 114 } 115 // Credentials is expected to manage caching internally if the metadata is fetched over 116 // the network. 117 creds.getRequestMetadata(uri, appExecutor, new RequestMetadataCallback() { 118 @Override 119 public void onSuccess(Map<String, List<String>> metadata) { 120 // Some implementations may pass null metadata. 121 122 // Re-use the headers if getRequestMetadata() returns the same map. It may return a 123 // different map based on the provided URI, i.e., for JWT. However, today it does not 124 // cache JWT and so we won't bother tring to save its return value based on the URI. 125 Metadata headers; 126 try { 127 synchronized (GoogleAuthLibraryCallCredentials.this) { 128 if (lastMetadata == null || lastMetadata != metadata) { 129 lastHeaders = toHeaders(metadata); 130 lastMetadata = metadata; 131 } 132 headers = lastHeaders; 133 } 134 } catch (Throwable t) { 135 applier.fail(Status.UNAUTHENTICATED 136 .withDescription("Failed to convert credential metadata") 137 .withCause(t)); 138 return; 139 } 140 applier.apply(headers); 141 } 142 143 @Override 144 public void onFailure(Throwable e) { 145 if (e instanceof IOException) { 146 // Since it's an I/O failure, let the call be retried with UNAVAILABLE. 147 applier.fail(Status.UNAVAILABLE 148 .withDescription("Credentials failed to obtain metadata") 149 .withCause(e)); 150 } else { 151 applier.fail(Status.UNAUTHENTICATED 152 .withDescription("Failed computing credential metadata") 153 .withCause(e)); 154 } 155 } 156 }); 157 } 158 159 /** 160 * Generate a JWT-specific service URI. The URI is simply an identifier with enough information 161 * for a service to know that the JWT was intended for it. The URI will commonly be verified with 162 * a simple string equality check. 163 */ serviceUri(String authority, MethodDescriptor<?, ?> method)164 private static URI serviceUri(String authority, MethodDescriptor<?, ?> method) 165 throws StatusException { 166 // Always use HTTPS, by definition. 167 final String scheme = "https"; 168 final int defaultPort = 443; 169 String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName()); 170 URI uri; 171 try { 172 uri = new URI(scheme, authority, path, null, null); 173 } catch (URISyntaxException e) { 174 throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth") 175 .withCause(e).asException(); 176 } 177 // The default port must not be present. Alternative ports should be present. 178 if (uri.getPort() == defaultPort) { 179 uri = removePort(uri); 180 } 181 return uri; 182 } 183 removePort(URI uri)184 private static URI removePort(URI uri) throws StatusException { 185 try { 186 return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */, 187 uri.getPath(), uri.getQuery(), uri.getFragment()); 188 } catch (URISyntaxException e) { 189 throw Status.UNAUTHENTICATED.withDescription( 190 "Unable to construct service URI after removing port").withCause(e).asException(); 191 } 192 } 193 194 @SuppressWarnings("BetaApi") // BaseEncoding is stable in Guava 20.0 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 @VisibleForTesting 256 static class JwtHelper { 257 private final Class<? extends Credentials> serviceAccountClass; 258 private final Constructor<? extends Credentials> jwtConstructor; 259 private final Method getScopes; 260 private final Method getClientId; 261 private final Method getClientEmail; 262 private final Method getPrivateKey; 263 private final Method getPrivateKeyId; 264 JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader)265 public JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader) 266 throws ClassNotFoundException, NoSuchMethodException { 267 serviceAccountClass = rawServiceAccountClass.asSubclass(Credentials.class); 268 getScopes = serviceAccountClass.getMethod("getScopes"); 269 getClientId = serviceAccountClass.getMethod("getClientId"); 270 getClientEmail = serviceAccountClass.getMethod("getClientEmail"); 271 getPrivateKey = serviceAccountClass.getMethod("getPrivateKey"); 272 getPrivateKeyId = serviceAccountClass.getMethod("getPrivateKeyId"); 273 Class<? extends Credentials> jwtClass = Class.forName( 274 "com.google.auth.oauth2.ServiceAccountJwtAccessCredentials", false, loader) 275 .asSubclass(Credentials.class); 276 jwtConstructor 277 = jwtClass.getConstructor(String.class, String.class, PrivateKey.class, String.class); 278 } 279 tryServiceAccountToJwt(Credentials creds)280 public Credentials tryServiceAccountToJwt(Credentials creds) { 281 if (!serviceAccountClass.isInstance(creds)) { 282 return creds; 283 } 284 Exception caughtException; 285 try { 286 creds = serviceAccountClass.cast(creds); 287 Collection<?> scopes = (Collection<?>) getScopes.invoke(creds); 288 if (scopes.size() != 0) { 289 // Leave as-is, since the scopes may limit access within the service. 290 return creds; 291 } 292 return jwtConstructor.newInstance( 293 getClientId.invoke(creds), 294 getClientEmail.invoke(creds), 295 getPrivateKey.invoke(creds), 296 getPrivateKeyId.invoke(creds)); 297 } catch (IllegalAccessException ex) { 298 caughtException = ex; 299 } catch (InvocationTargetException ex) { 300 caughtException = ex; 301 } catch (InstantiationException ex) { 302 caughtException = ex; 303 } 304 if (caughtException != null) { 305 // Failure is a bug in this class, but we still choose to gracefully recover 306 log.log( 307 Level.WARNING, 308 "Failed converting service account credential to JWT. This is unexpected", 309 caughtException); 310 } 311 return creds; 312 } 313 } 314 } 315