• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.google.auth.oauth2.GoogleCredentials.SERVICE_ACCOUNT_FILE_TYPE;
35 import static com.google.auth.oauth2.GoogleCredentials.addQuotaProjectIdToRequestMetadata;
36 
37 import com.google.api.client.json.GenericJson;
38 import com.google.api.client.json.JsonFactory;
39 import com.google.api.client.json.JsonObjectParser;
40 import com.google.api.client.util.Clock;
41 import com.google.api.client.util.Preconditions;
42 import com.google.auth.Credentials;
43 import com.google.auth.RequestMetadataCallback;
44 import com.google.auth.ServiceAccountSigner;
45 import com.google.common.annotations.VisibleForTesting;
46 import com.google.common.base.MoreObjects;
47 import com.google.common.base.Throwables;
48 import com.google.common.base.Ticker;
49 import com.google.common.cache.CacheBuilder;
50 import com.google.common.cache.CacheLoader;
51 import com.google.common.cache.LoadingCache;
52 import com.google.common.util.concurrent.UncheckedExecutionException;
53 import com.google.errorprone.annotations.CanIgnoreReturnValue;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.io.ObjectInputStream;
57 import java.net.URI;
58 import java.nio.charset.StandardCharsets;
59 import java.security.InvalidKeyException;
60 import java.security.NoSuchAlgorithmException;
61 import java.security.PrivateKey;
62 import java.security.Signature;
63 import java.security.SignatureException;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Objects;
67 import java.util.concurrent.ExecutionException;
68 import java.util.concurrent.Executor;
69 import java.util.concurrent.TimeUnit;
70 
71 /**
72  * Service Account credentials for calling Google APIs using a JWT directly for access.
73  *
74  * <p>Uses a JSON Web Token (JWT) directly in the request metadata to provide authorization.
75  */
76 public class ServiceAccountJwtAccessCredentials extends Credentials
77     implements JwtProvider, ServiceAccountSigner, QuotaProjectIdProvider {
78 
79   private static final long serialVersionUID = -7274955171379494197L;
80   static final String JWT_ACCESS_PREFIX = OAuth2Utils.BEARER_PREFIX;
81 
82   @VisibleForTesting static final long LIFE_SPAN_SECS = TimeUnit.HOURS.toSeconds(1);
83   private static final long CLOCK_SKEW = TimeUnit.MINUTES.toSeconds(5);
84 
85   private final String clientId;
86   private final String clientEmail;
87   private final PrivateKey privateKey;
88   private final String privateKeyId;
89   private final URI defaultAudience;
90   private final String quotaProjectId;
91 
92   private transient LoadingCache<JwtClaims, JwtCredentials> credentialsCache;
93 
94   // Until we expose this to the users it can remain transient and non-serializable
95   @VisibleForTesting transient Clock clock = Clock.SYSTEM;
96 
97   /**
98    * Constructor with minimum identifying information.
99    *
100    * @param clientId Client ID of the service account from the console. May be null.
101    * @param clientEmail Client email address of the service account from the console.
102    * @param privateKey RSA private key object for the service account.
103    * @param privateKeyId Private key identifier for the service account. May be null.
104    */
ServiceAccountJwtAccessCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId)105   private ServiceAccountJwtAccessCredentials(
106       String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId) {
107     this(clientId, clientEmail, privateKey, privateKeyId, null, null);
108   }
109 
110   /**
111    * Constructor with full information.
112    *
113    * @param clientId Client ID of the service account from the console. May be null.
114    * @param clientEmail Client email address of the service account from the console.
115    * @param privateKey RSA private key object for the service account.
116    * @param privateKeyId Private key identifier for the service account. May be null.
117    * @param defaultAudience Audience to use if not provided by transport. May be null.
118    */
ServiceAccountJwtAccessCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId, URI defaultAudience, String quotaProjectId)119   private ServiceAccountJwtAccessCredentials(
120       String clientId,
121       String clientEmail,
122       PrivateKey privateKey,
123       String privateKeyId,
124       URI defaultAudience,
125       String quotaProjectId) {
126     this.clientId = clientId;
127     this.clientEmail = Preconditions.checkNotNull(clientEmail);
128     this.privateKey = Preconditions.checkNotNull(privateKey);
129     this.privateKeyId = privateKeyId;
130     this.defaultAudience = defaultAudience;
131     this.credentialsCache = createCache();
132     this.quotaProjectId = quotaProjectId;
133   }
134 
135   /**
136    * Returns service account credentials defined by JSON using the format supported by the Google
137    * Developers Console.
138    *
139    * @param json a map from the JSON representing the credentials.
140    * @return the credentials defined by the JSON.
141    * @throws IOException if the credential cannot be created from the JSON.
142    */
fromJson(Map<String, Object> json)143   static ServiceAccountJwtAccessCredentials fromJson(Map<String, Object> json) throws IOException {
144     return fromJson(json, null);
145   }
146 
147   /**
148    * Returns service account credentials defined by JSON using the format supported by the Google
149    * Developers Console.
150    *
151    * @param json a map from the JSON representing the credentials.
152    * @param defaultAudience Audience to use if not provided by transport. May be null.
153    * @return the credentials defined by the JSON.
154    * @throws IOException if the credential cannot be created from the JSON.
155    */
fromJson(Map<String, Object> json, URI defaultAudience)156   static ServiceAccountJwtAccessCredentials fromJson(Map<String, Object> json, URI defaultAudience)
157       throws IOException {
158     String clientId = (String) json.get("client_id");
159     String clientEmail = (String) json.get("client_email");
160     String privateKeyPkcs8 = (String) json.get("private_key");
161     String privateKeyId = (String) json.get("private_key_id");
162     String quoataProjectId = (String) json.get("quota_project_id");
163     if (clientId == null
164         || clientEmail == null
165         || privateKeyPkcs8 == null
166         || privateKeyId == null) {
167       throw new IOException(
168           "Error reading service account credential from JSON, "
169               + "expecting  'client_id', 'client_email', 'private_key' and 'private_key_id'.");
170     }
171     return ServiceAccountJwtAccessCredentials.fromPkcs8(
172         clientId, clientEmail, privateKeyPkcs8, privateKeyId, defaultAudience, quoataProjectId);
173   }
174 
175   /**
176    * Factory using PKCS#8 for the private key.
177    *
178    * @param clientId Client ID of the service account from the console. May be null.
179    * @param clientEmail Client email address of the service account from the console.
180    * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format.
181    * @param privateKeyId Private key identifier for the service account. May be null.
182    * @return New ServiceAccountJwtAcceessCredentials created from a private key.
183    * @throws IOException if the credential cannot be created from the private key.
184    */
fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId)185   public static ServiceAccountJwtAccessCredentials fromPkcs8(
186       String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId)
187       throws IOException {
188     return fromPkcs8(clientId, clientEmail, privateKeyPkcs8, privateKeyId, null);
189   }
190 
191   /**
192    * Factory using PKCS#8 for the private key.
193    *
194    * @param clientId Client ID of the service account from the console. May be null.
195    * @param clientEmail Client email address of the service account from the console.
196    * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format.
197    * @param privateKeyId Private key identifier for the service account. May be null.
198    * @param defaultAudience Audience to use if not provided by transport. May be null.
199    * @return New ServiceAccountJwtAcceessCredentials created from a private key.
200    * @throws IOException if the credential cannot be created from the private key.
201    */
fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, URI defaultAudience)202   public static ServiceAccountJwtAccessCredentials fromPkcs8(
203       String clientId,
204       String clientEmail,
205       String privateKeyPkcs8,
206       String privateKeyId,
207       URI defaultAudience)
208       throws IOException {
209     return ServiceAccountJwtAccessCredentials.fromPkcs8(
210         clientId, clientEmail, privateKeyPkcs8, privateKeyId, defaultAudience, null);
211   }
212 
fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, URI defaultAudience, String quotaProjectId)213   static ServiceAccountJwtAccessCredentials fromPkcs8(
214       String clientId,
215       String clientEmail,
216       String privateKeyPkcs8,
217       String privateKeyId,
218       URI defaultAudience,
219       String quotaProjectId)
220       throws IOException {
221     PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
222     return new ServiceAccountJwtAccessCredentials(
223         clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId);
224   }
225 
226   /**
227    * Returns credentials defined by a Service Account key file in JSON format from the Google
228    * Developers Console.
229    *
230    * @param credentialsStream the stream with the credential definition.
231    * @return the credential defined by the credentialsStream.
232    * @throws IOException if the credential cannot be created from the stream.
233    */
fromStream(InputStream credentialsStream)234   public static ServiceAccountJwtAccessCredentials fromStream(InputStream credentialsStream)
235       throws IOException {
236     return fromStream(credentialsStream, null);
237   }
238 
239   /**
240    * Returns credentials defined by a Service Account key file in JSON format from the Google
241    * Developers Console.
242    *
243    * @param credentialsStream the stream with the credential definition.
244    * @param defaultAudience Audience to use if not provided by transport. May be null.
245    * @return the credential defined by the credentialsStream.
246    * @throws IOException if the credential cannot be created from the stream.
247    */
fromStream( InputStream credentialsStream, URI defaultAudience)248   public static ServiceAccountJwtAccessCredentials fromStream(
249       InputStream credentialsStream, URI defaultAudience) throws IOException {
250     Preconditions.checkNotNull(credentialsStream);
251 
252     JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
253     JsonObjectParser parser = new JsonObjectParser(jsonFactory);
254     GenericJson fileContents =
255         parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
256 
257     String fileType = (String) fileContents.get("type");
258     if (fileType == null) {
259       throw new IOException("Error reading credentials from stream, 'type' field not specified.");
260     }
261     if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
262       return fromJson(fileContents, defaultAudience);
263     }
264     throw new IOException(
265         String.format(
266             "Error reading credentials from stream, 'type' value '%s' not recognized."
267                 + " Expecting '%s'.",
268             fileType, SERVICE_ACCOUNT_FILE_TYPE));
269   }
270 
createCache()271   private LoadingCache<JwtClaims, JwtCredentials> createCache() {
272     return CacheBuilder.newBuilder()
273         .maximumSize(100)
274         .expireAfterWrite(LIFE_SPAN_SECS - CLOCK_SKEW, TimeUnit.SECONDS)
275         .ticker(
276             new Ticker() {
277               @Override
278               public long read() {
279                 return TimeUnit.MILLISECONDS.toNanos(clock.currentTimeMillis());
280               }
281             })
282         .build(
283             new CacheLoader<JwtClaims, JwtCredentials>() {
284               @Override
285               public JwtCredentials load(JwtClaims claims) throws Exception {
286                 return JwtCredentials.newBuilder()
287                     .setPrivateKey(privateKey)
288                     .setPrivateKeyId(privateKeyId)
289                     .setJwtClaims(claims)
290                     .setLifeSpanSeconds(LIFE_SPAN_SECS)
291                     .setClock(clock)
292                     .build();
293               }
294             });
295   }
296 
297   /**
298    * Returns a new JwtCredentials instance with modified claims.
299    *
300    * @param newClaims new claims. Any unspecified claim fields will default to the the current
301    *     values.
302    * @return new credentials
303    */
304   @Override
305   public JwtCredentials jwtWithClaims(JwtClaims newClaims) {
306     JwtClaims.Builder claimsBuilder =
307         JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);
308     if (defaultAudience != null) {
309       claimsBuilder.setAudience(defaultAudience.toString());
310     }
311     return JwtCredentials.newBuilder()
312         .setPrivateKey(privateKey)
313         .setPrivateKeyId(privateKeyId)
314         .setJwtClaims(claimsBuilder.build().merge(newClaims))
315         .setLifeSpanSeconds(LIFE_SPAN_SECS)
316         .setClock(clock)
317         .build();
318   }
319 
320   @Override
321   public String getAuthenticationType() {
322     return "JWTAccess";
323   }
324 
325   @Override
326   public boolean hasRequestMetadata() {
327     return true;
328   }
329 
330   @Override
331   public boolean hasRequestMetadataOnly() {
332     return true;
333   }
334 
335   @Override
336   public void getRequestMetadata(
337       final URI uri, Executor executor, final RequestMetadataCallback callback) {
338     // It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferrable
339     // to do it in the current thread, which is likely to be the network thread.
340     blockingGetToCallback(uri, callback);
341   }
342 
343   /** Provide the request metadata by putting an access JWT directly in the metadata. */
344   @Override
345   public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
346     if (uri == null) {
347       if (defaultAudience != null) {
348         uri = defaultAudience;
349       } else {
350         throw new IOException(
351             "JwtAccess requires Audience uri to be passed in or the "
352                 + "defaultAudience to be specified");
353       }
354     }
355 
356     try {
357       JwtClaims defaultClaims =
358           JwtClaims.newBuilder()
359               .setAudience(uri.toString())
360               .setIssuer(clientEmail)
361               .setSubject(clientEmail)
362               .build();
363       JwtCredentials credentials = credentialsCache.get(defaultClaims);
364       Map<String, List<String>> requestMetadata = credentials.getRequestMetadata(uri);
365       return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
366     } catch (ExecutionException e) {
367       Throwables.propagateIfPossible(e.getCause(), IOException.class);
368       // Should never happen
369       throw new IllegalStateException(
370           "generateJwtAccess threw an unexpected checked exception", e.getCause());
371 
372     } catch (UncheckedExecutionException e) {
373       Throwables.throwIfUnchecked(e);
374       // Should never happen
375       throw new IllegalStateException(
376           "generateJwtAccess threw an unchecked exception that couldn't be rethrown", e);
377     }
378   }
379 
380   /** Discard any cached data */
381   @Override
382   public void refresh() {
383     credentialsCache.invalidateAll();
384   }
385 
386   public final String getClientId() {
387     return clientId;
388   }
389 
390   public final String getClientEmail() {
391     return clientEmail;
392   }
393 
394   public final PrivateKey getPrivateKey() {
395     return privateKey;
396   }
397 
398   public final String getPrivateKeyId() {
399     return privateKeyId;
400   }
401 
402   @Override
403   public String getAccount() {
404     return getClientEmail();
405   }
406 
407   @Override
408   public byte[] sign(byte[] toSign) {
409     try {
410       Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM);
411       signer.initSign(getPrivateKey());
412       signer.update(toSign);
413       return signer.sign();
414     } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) {
415       throw new ServiceAccountSigner.SigningException("Failed to sign the provided bytes", ex);
416     }
417   }
418 
419   @Override
420   public int hashCode() {
421     return Objects.hash(
422         clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId);
423   }
424 
425   @Override
426   public String toString() {
427     return MoreObjects.toStringHelper(this)
428         .add("clientId", clientId)
429         .add("clientEmail", clientEmail)
430         .add("privateKeyId", privateKeyId)
431         .add("defaultAudience", defaultAudience)
432         .add("quotaProjectId", quotaProjectId)
433         .toString();
434   }
435 
436   @Override
437   public boolean equals(Object obj) {
438     if (!(obj instanceof ServiceAccountJwtAccessCredentials)) {
439       return false;
440     }
441     ServiceAccountJwtAccessCredentials other = (ServiceAccountJwtAccessCredentials) obj;
442     return Objects.equals(this.clientId, other.clientId)
443         && Objects.equals(this.clientEmail, other.clientEmail)
444         && Objects.equals(this.privateKey, other.privateKey)
445         && Objects.equals(this.privateKeyId, other.privateKeyId)
446         && Objects.equals(this.defaultAudience, other.defaultAudience)
447         && Objects.equals(this.quotaProjectId, other.quotaProjectId);
448   }
449 
450   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
451     input.defaultReadObject();
452     clock = Clock.SYSTEM;
453     credentialsCache = createCache();
454   }
455 
456   public static Builder newBuilder() {
457     return new Builder();
458   }
459 
460   public Builder toBuilder() {
461     return new Builder(this);
462   }
463 
464   @Override
465   public String getQuotaProjectId() {
466     return quotaProjectId;
467   }
468 
469   public static class Builder {
470 
471     private String clientId;
472     private String clientEmail;
473     private PrivateKey privateKey;
474     private String privateKeyId;
475     private URI defaultAudience;
476     private String quotaProjectId;
477 
478     protected Builder() {}
479 
480     protected Builder(ServiceAccountJwtAccessCredentials credentials) {
481       this.clientId = credentials.clientId;
482       this.clientEmail = credentials.clientEmail;
483       this.privateKey = credentials.privateKey;
484       this.privateKeyId = credentials.privateKeyId;
485       this.defaultAudience = credentials.defaultAudience;
486       this.quotaProjectId = credentials.quotaProjectId;
487     }
488 
489     @CanIgnoreReturnValue
490     public Builder setClientId(String clientId) {
491       this.clientId = clientId;
492       return this;
493     }
494 
495     @CanIgnoreReturnValue
496     public Builder setClientEmail(String clientEmail) {
497       this.clientEmail = clientEmail;
498       return this;
499     }
500 
501     @CanIgnoreReturnValue
502     public Builder setPrivateKey(PrivateKey privateKey) {
503       this.privateKey = privateKey;
504       return this;
505     }
506 
507     @CanIgnoreReturnValue
508     public Builder setPrivateKeyId(String privateKeyId) {
509       this.privateKeyId = privateKeyId;
510       return this;
511     }
512 
513     @CanIgnoreReturnValue
514     public Builder setDefaultAudience(URI defaultAudience) {
515       this.defaultAudience = defaultAudience;
516       return this;
517     }
518 
519     @CanIgnoreReturnValue
520     public Builder setQuotaProjectId(String quotaProjectId) {
521       this.quotaProjectId = quotaProjectId;
522       return this;
523     }
524 
525     public String getClientId() {
526       return clientId;
527     }
528 
529     public String getClientEmail() {
530       return clientEmail;
531     }
532 
533     public PrivateKey getPrivateKey() {
534       return privateKey;
535     }
536 
537     public String getPrivateKeyId() {
538       return privateKeyId;
539     }
540 
541     public URI getDefaultAudience() {
542       return defaultAudience;
543     }
544 
545     public String getQuotaProjectId() {
546       return quotaProjectId;
547     }
548 
549     public ServiceAccountJwtAccessCredentials build() {
550       return new ServiceAccountJwtAccessCredentials(
551           clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId);
552     }
553   }
554 }
555