• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CallCredentials2;
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.Constructor;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.net.URI;
36 import java.net.URISyntaxException;
37 import java.security.PrivateKey;
38 import java.util.Collection;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.concurrent.Executor;
42 import java.util.logging.Level;
43 import java.util.logging.Logger;
44 import javax.annotation.Nullable;
45 
46 /**
47  * Wraps {@link Credentials} as a {@link CallCredentials}.
48  */
49 final class GoogleAuthLibraryCallCredentials extends CallCredentials2 {
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> googleCredentialsClass
55       = loadGoogleCredentialsClass();
56 
57   private final boolean requirePrivacy;
58   @VisibleForTesting
59   final Credentials creds;
60 
61   private Metadata lastHeaders;
62   private Map<String, List<String>> lastMetadata;
63 
GoogleAuthLibraryCallCredentials(Credentials creds)64   public GoogleAuthLibraryCallCredentials(Credentials creds) {
65     this(creds, jwtHelper);
66   }
67 
68   @VisibleForTesting
GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper)69   GoogleAuthLibraryCallCredentials(Credentials creds, JwtHelper jwtHelper) {
70     checkNotNull(creds, "creds");
71     boolean requirePrivacy = false;
72     if (googleCredentialsClass != null) {
73       // All GoogleCredentials instances are bearer tokens and should only be used on private
74       // channels. This catches all return values from GoogleCredentials.getApplicationDefault().
75       // This should be checked before upgrading the Service Account to JWT, as JWT is also a bearer
76       // token.
77       requirePrivacy = googleCredentialsClass.isInstance(creds);
78     }
79     if (jwtHelper != null) {
80       creds = jwtHelper.tryServiceAccountToJwt(creds);
81     }
82     this.requirePrivacy = requirePrivacy;
83     this.creds = creds;
84   }
85 
86   @Override
thisUsesUnstableApi()87   public void thisUsesUnstableApi() {}
88 
89   @Override
applyRequestMetadata( RequestInfo info, Executor appExecutor, final MetadataApplier applier)90   public void applyRequestMetadata(
91       RequestInfo info, Executor appExecutor, final MetadataApplier applier) {
92     SecurityLevel security = info.getSecurityLevel();
93     if (requirePrivacy && security != SecurityLevel.PRIVACY_AND_INTEGRITY) {
94       applier.fail(Status.UNAUTHENTICATED
95           .withDescription("Credentials require channel with PRIVACY_AND_INTEGRITY security level. "
96               + "Observed security level: " + security));
97       return;
98     }
99 
100     String authority = checkNotNull(info.getAuthority(), "authority");
101     final URI uri;
102     try {
103       uri = serviceUri(authority, info.getMethodDescriptor());
104     } catch (StatusException e) {
105       applier.fail(e.getStatus());
106       return;
107     }
108     // Credentials is expected to manage caching internally if the metadata is fetched over
109     // the network.
110     creds.getRequestMetadata(uri, appExecutor, new RequestMetadataCallback() {
111       @Override
112       public void onSuccess(Map<String, List<String>> metadata) {
113         // Some implementations may pass null metadata.
114 
115         // Re-use the headers if getRequestMetadata() returns the same map. It may return a
116         // different map based on the provided URI, i.e., for JWT. However, today it does not
117         // cache JWT and so we won't bother tring to save its return value based on the URI.
118         Metadata headers;
119         try {
120           synchronized (GoogleAuthLibraryCallCredentials.this) {
121             if (lastMetadata == null || lastMetadata != metadata) {
122               lastHeaders = toHeaders(metadata);
123               lastMetadata = metadata;
124             }
125             headers = lastHeaders;
126           }
127         } catch (Throwable t) {
128           applier.fail(Status.UNAUTHENTICATED
129               .withDescription("Failed to convert credential metadata")
130               .withCause(t));
131           return;
132         }
133         applier.apply(headers);
134       }
135 
136       @Override
137       public void onFailure(Throwable e) {
138         if (e instanceof IOException) {
139           // Since it's an I/O failure, let the call be retried with UNAVAILABLE.
140           applier.fail(Status.UNAVAILABLE
141               .withDescription("Credentials failed to obtain metadata")
142               .withCause(e));
143         } else {
144           applier.fail(Status.UNAUTHENTICATED
145               .withDescription("Failed computing credential metadata")
146               .withCause(e));
147         }
148       }
149     });
150   }
151 
152   /**
153    * Generate a JWT-specific service URI. The URI is simply an identifier with enough information
154    * for a service to know that the JWT was intended for it. The URI will commonly be verified with
155    * a simple string equality check.
156    */
serviceUri(String authority, MethodDescriptor<?, ?> method)157   private static URI serviceUri(String authority, MethodDescriptor<?, ?> method)
158       throws StatusException {
159     // Always use HTTPS, by definition.
160     final String scheme = "https";
161     final int defaultPort = 443;
162     String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
163     URI uri;
164     try {
165       uri = new URI(scheme, authority, path, null, null);
166     } catch (URISyntaxException e) {
167       throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth")
168           .withCause(e).asException();
169     }
170     // The default port must not be present. Alternative ports should be present.
171     if (uri.getPort() == defaultPort) {
172       uri = removePort(uri);
173     }
174     return uri;
175   }
176 
removePort(URI uri)177   private static URI removePort(URI uri) throws StatusException {
178     try {
179       return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
180           uri.getPath(), uri.getQuery(), uri.getFragment());
181     } catch (URISyntaxException e) {
182       throw Status.UNAUTHENTICATED.withDescription(
183            "Unable to construct service URI after removing port").withCause(e).asException();
184     }
185   }
186 
187   @SuppressWarnings("BetaApi") // BaseEncoding is stable in Guava 20.0
toHeaders(@ullable Map<String, List<String>> metadata)188   private static Metadata toHeaders(@Nullable Map<String, List<String>> metadata) {
189     Metadata headers = new Metadata();
190     if (metadata != null) {
191       for (String key : metadata.keySet()) {
192         if (key.endsWith("-bin")) {
193           Metadata.Key<byte[]> headerKey = Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER);
194           for (String value : metadata.get(key)) {
195             headers.put(headerKey, BaseEncoding.base64().decode(value));
196           }
197         } else {
198           Metadata.Key<String> headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
199           for (String value : metadata.get(key)) {
200             headers.put(headerKey, value);
201           }
202         }
203       }
204     }
205     return headers;
206   }
207 
208   @VisibleForTesting
209   @Nullable
createJwtHelperOrNull(ClassLoader loader)210   static JwtHelper createJwtHelperOrNull(ClassLoader loader) {
211     Class<?> rawServiceAccountClass;
212     try {
213       // Specify loader so it can be overridden in tests
214       rawServiceAccountClass
215           = Class.forName("com.google.auth.oauth2.ServiceAccountCredentials", false, loader);
216     } catch (ClassNotFoundException ex) {
217       return null;
218     }
219     Exception caughtException;
220     try {
221       return new JwtHelper(rawServiceAccountClass, loader);
222     } catch (ClassNotFoundException ex) {
223       caughtException = ex;
224     } catch (NoSuchMethodException ex) {
225       caughtException = ex;
226     }
227     if (caughtException != null) {
228       // Failure is a bug in this class, but we still choose to gracefully recover
229       log.log(Level.WARNING, "Failed to create JWT helper. This is unexpected", caughtException);
230     }
231     return null;
232   }
233 
234   @Nullable
loadGoogleCredentialsClass()235   private static Class<? extends Credentials> loadGoogleCredentialsClass() {
236     Class<?> rawGoogleCredentialsClass;
237     try {
238       // Can't use a loader as it disables ProGuard's reference detection and would fail to rename
239       // this reference. Unfortunately this will initialize the class.
240       rawGoogleCredentialsClass = Class.forName("com.google.auth.oauth2.GoogleCredentials");
241     } catch (ClassNotFoundException ex) {
242       log.log(Level.FINE, "Failed to load GoogleCredentials", ex);
243       return null;
244     }
245     return rawGoogleCredentialsClass.asSubclass(Credentials.class);
246   }
247 
248   @VisibleForTesting
249   static class JwtHelper {
250     private final Class<? extends Credentials> serviceAccountClass;
251     private final Constructor<? extends Credentials> jwtConstructor;
252     private final Method getScopes;
253     private final Method getClientId;
254     private final Method getClientEmail;
255     private final Method getPrivateKey;
256     private final Method getPrivateKeyId;
257 
JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader)258     public JwtHelper(Class<?> rawServiceAccountClass, ClassLoader loader)
259         throws ClassNotFoundException, NoSuchMethodException {
260       serviceAccountClass = rawServiceAccountClass.asSubclass(Credentials.class);
261       getScopes = serviceAccountClass.getMethod("getScopes");
262       getClientId = serviceAccountClass.getMethod("getClientId");
263       getClientEmail = serviceAccountClass.getMethod("getClientEmail");
264       getPrivateKey = serviceAccountClass.getMethod("getPrivateKey");
265       getPrivateKeyId = serviceAccountClass.getMethod("getPrivateKeyId");
266       Class<? extends Credentials> jwtClass = Class.forName(
267           "com.google.auth.oauth2.ServiceAccountJwtAccessCredentials", false, loader)
268           .asSubclass(Credentials.class);
269       jwtConstructor
270           = jwtClass.getConstructor(String.class, String.class, PrivateKey.class, String.class);
271     }
272 
tryServiceAccountToJwt(Credentials creds)273     public Credentials tryServiceAccountToJwt(Credentials creds) {
274       if (!serviceAccountClass.isInstance(creds)) {
275         return creds;
276       }
277       Exception caughtException;
278       try {
279         creds = serviceAccountClass.cast(creds);
280         Collection<?> scopes = (Collection<?>) getScopes.invoke(creds);
281         if (scopes.size() != 0) {
282           // Leave as-is, since the scopes may limit access within the service.
283           return creds;
284         }
285         return jwtConstructor.newInstance(
286             getClientId.invoke(creds),
287             getClientEmail.invoke(creds),
288             getPrivateKey.invoke(creds),
289             getPrivateKeyId.invoke(creds));
290       } catch (IllegalAccessException ex) {
291         caughtException = ex;
292       } catch (InvocationTargetException ex) {
293         caughtException = ex;
294       } catch (InstantiationException ex) {
295         caughtException = ex;
296       }
297       if (caughtException != null) {
298         // Failure is a bug in this class, but we still choose to gracefully recover
299         log.log(
300             Level.WARNING,
301             "Failed converting service account credential to JWT. This is unexpected",
302             caughtException);
303       }
304       return creds;
305     }
306   }
307 }
308