• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018, 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.common.base.MoreObjects.firstNonNull;
35 import static com.google.common.base.Preconditions.checkNotNull;
36 
37 import com.google.api.client.http.GenericUrl;
38 import com.google.api.client.http.HttpContent;
39 import com.google.api.client.http.HttpRequest;
40 import com.google.api.client.http.HttpRequestFactory;
41 import com.google.api.client.http.HttpResponse;
42 import com.google.api.client.http.HttpTransport;
43 import com.google.api.client.http.json.JsonHttpContent;
44 import com.google.api.client.json.JsonObjectParser;
45 import com.google.api.client.util.GenericData;
46 import com.google.auth.ServiceAccountSigner;
47 import com.google.auth.http.HttpCredentialsAdapter;
48 import com.google.auth.http.HttpTransportFactory;
49 import com.google.common.annotations.VisibleForTesting;
50 import com.google.common.base.MoreObjects;
51 import com.google.common.collect.ImmutableMap;
52 import com.google.errorprone.annotations.CanIgnoreReturnValue;
53 import java.io.IOException;
54 import java.io.ObjectInputStream;
55 import java.text.DateFormat;
56 import java.text.ParseException;
57 import java.text.SimpleDateFormat;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.Calendar;
61 import java.util.Collection;
62 import java.util.Date;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.Objects;
66 
67 /**
68  * ImpersonatedCredentials allowing credentials issued to a user or service account to impersonate
69  * another. The source project using ImpersonatedCredentials must enable the "IAMCredentials" API.
70  * Also, the target service account must grant the originating principal the "Service Account Token
71  * Creator" IAM role.
72  *
73  * <p>Usage:
74  *
75  * <pre>
76  * String credPath = "/path/to/svc_account.json";
77  * ServiceAccountCredentials sourceCredentials = ServiceAccountCredentials
78  *     .fromStream(new FileInputStream(credPath));
79  * sourceCredentials = (ServiceAccountCredentials) sourceCredentials
80  *     .createScoped(Arrays.asList("https://www.googleapis.com/auth/iam"));
81  *
82  * ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials,
83  *     "impersonated-account@project.iam.gserviceaccount.com", null,
84  *     Arrays.asList("https://www.googleapis.com/auth/devstorage.read_only"), 300);
85  *
86  * Storage storage_service = StorageOptions.newBuilder().setProjectId("project-id")
87  *    .setCredentials(targetCredentials).build().getService();
88  *
89  * for (Bucket b : storage_service.list().iterateAll())
90  *     System.out.println(b);
91  * </pre>
92  */
93 public class ImpersonatedCredentials extends GoogleCredentials
94     implements ServiceAccountSigner, IdTokenProvider {
95 
96   static final String IMPERSONATED_CREDENTIALS_FILE_TYPE = "impersonated_service_account";
97 
98   private static final long serialVersionUID = -2133257318957488431L;
99   private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
100   private static final int TWELVE_HOURS_IN_SECONDS = 43200;
101   private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
102   private static final String CLOUD_PLATFORM_SCOPE =
103       "https://www.googleapis.com/auth/cloud-platform";
104   private static final String IAM_ACCESS_TOKEN_ENDPOINT =
105       "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
106 
107   private GoogleCredentials sourceCredentials;
108   private String targetPrincipal;
109   private List<String> delegates;
110   private List<String> scopes;
111   private int lifetime;
112   private String iamEndpointOverride;
113   private final String transportFactoryClassName;
114 
115   private transient HttpTransportFactory transportFactory;
116 
117   private transient Calendar calendar;
118 
119   /**
120    * @param sourceCredentials the source credential used to acquire the impersonated credentials. It
121    *     should be either a user account credential or a service account credential.
122    * @param targetPrincipal the service account to impersonate
123    * @param delegates the chained list of delegates required to grant the final access_token. If
124    *     set, the sequence of identities must have "Service Account Token Creator" capability
125    *     granted to the preceding identity. For example, if set to [serviceAccountB,
126    *     serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
127    *     serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
128    *     Creator on target_principal. If unset, sourceCredential must have that role on
129    *     targetPrincipal.
130    * @param scopes scopes to request during the authorization grant
131    * @param lifetime number of seconds the delegated credential should be valid. By default this
132    *     value should be at most 3600. However, you can follow <a
133    *     href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
134    *     instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
135    *     hours). If the given lifetime is 0, default value 3600 will be used instead when creating
136    *     the credentials.
137    * @param transportFactory HTTP transport factory that creates the transport used to get access
138    *     tokens
139    * @return new credentials
140    */
create( GoogleCredentials sourceCredentials, String targetPrincipal, List<String> delegates, List<String> scopes, int lifetime, HttpTransportFactory transportFactory)141   public static ImpersonatedCredentials create(
142       GoogleCredentials sourceCredentials,
143       String targetPrincipal,
144       List<String> delegates,
145       List<String> scopes,
146       int lifetime,
147       HttpTransportFactory transportFactory) {
148     return ImpersonatedCredentials.newBuilder()
149         .setSourceCredentials(sourceCredentials)
150         .setTargetPrincipal(targetPrincipal)
151         .setDelegates(delegates)
152         .setScopes(scopes)
153         .setLifetime(lifetime)
154         .setHttpTransportFactory(transportFactory)
155         .build();
156   }
157 
158   /**
159    * @param sourceCredentials the source credential used to acquire the impersonated credentials. It
160    *     should be either a user account credential or a service account credential.
161    * @param targetPrincipal the service account to impersonate
162    * @param delegates the chained list of delegates required to grant the final access_token. If
163    *     set, the sequence of identities must have "Service Account Token Creator" capability
164    *     granted to the preceding identity. For example, if set to [serviceAccountB,
165    *     serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
166    *     serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
167    *     Creator on target_principal. If unset, sourceCredential must have that role on
168    *     targetPrincipal.
169    * @param scopes scopes to request during the authorization grant
170    * @param lifetime number of seconds the delegated credential should be valid. By default this
171    *     value should be at most 3600. However, you can follow <a
172    *     href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
173    *     instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
174    *     hours). If the given lifetime is 0, default value 3600 will be used instead when creating
175    *     the credentials.
176    * @param transportFactory HTTP transport factory that creates the transport used to get access
177    *     tokens.
178    * @param quotaProjectId the project used for quota and billing purposes. Should be null unless
179    *     the caller wants to use a project different from the one that owns the impersonated
180    *     credential for billing/quota purposes.
181    * @return new credentials
182    */
create( GoogleCredentials sourceCredentials, String targetPrincipal, List<String> delegates, List<String> scopes, int lifetime, HttpTransportFactory transportFactory, String quotaProjectId)183   public static ImpersonatedCredentials create(
184       GoogleCredentials sourceCredentials,
185       String targetPrincipal,
186       List<String> delegates,
187       List<String> scopes,
188       int lifetime,
189       HttpTransportFactory transportFactory,
190       String quotaProjectId) {
191     return ImpersonatedCredentials.newBuilder()
192         .setSourceCredentials(sourceCredentials)
193         .setTargetPrincipal(targetPrincipal)
194         .setDelegates(delegates)
195         .setScopes(scopes)
196         .setLifetime(lifetime)
197         .setHttpTransportFactory(transportFactory)
198         .setQuotaProjectId(quotaProjectId)
199         .build();
200   }
201 
202   /**
203    * @param sourceCredentials the source credential used to acquire the impersonated credentials. It
204    *     should be either a user account credential or a service account credential.
205    * @param targetPrincipal the service account to impersonate
206    * @param delegates the chained list of delegates required to grant the final access_token. If
207    *     set, the sequence of identities must have "Service Account Token Creator" capability
208    *     granted to the preceding identity. For example, if set to [serviceAccountB,
209    *     serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
210    *     serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
211    *     Creator on target_principal. If unset, sourceCredential must have that role on
212    *     targetPrincipal.
213    * @param scopes scopes to request during the authorization grant
214    * @param lifetime number of seconds the delegated credential should be valid. By default this
215    *     value should be at most 3600. However, you can follow <a
216    *     href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
217    *     instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
218    *     hours). If the given lifetime is 0, default value 3600 will be used instead when creating
219    *     the credentials.
220    * @param transportFactory HTTP transport factory that creates the transport used to get access
221    *     tokens.
222    * @param quotaProjectId the project used for quota and billing purposes. Should be null unless
223    *     the caller wants to use a project different from the one that owns the impersonated
224    *     credential for billing/quota purposes.
225    * @param iamEndpointOverride The full IAM endpoint override with the target_principal embedded.
226    *     This is useful when supporting impersonation with regional endpoints.
227    * @return new credentials
228    */
create( GoogleCredentials sourceCredentials, String targetPrincipal, List<String> delegates, List<String> scopes, int lifetime, HttpTransportFactory transportFactory, String quotaProjectId, String iamEndpointOverride)229   public static ImpersonatedCredentials create(
230       GoogleCredentials sourceCredentials,
231       String targetPrincipal,
232       List<String> delegates,
233       List<String> scopes,
234       int lifetime,
235       HttpTransportFactory transportFactory,
236       String quotaProjectId,
237       String iamEndpointOverride) {
238     return ImpersonatedCredentials.newBuilder()
239         .setSourceCredentials(sourceCredentials)
240         .setTargetPrincipal(targetPrincipal)
241         .setDelegates(delegates)
242         .setScopes(scopes)
243         .setLifetime(lifetime)
244         .setHttpTransportFactory(transportFactory)
245         .setQuotaProjectId(quotaProjectId)
246         .setIamEndpointOverride(iamEndpointOverride)
247         .build();
248   }
249 
250   /**
251    * @param sourceCredentials the source credential used to acquire the impersonated credentials. It
252    *     should be either a user account credential or a service account credential.
253    * @param targetPrincipal the service account to impersonate
254    * @param delegates the chained list of delegates required to grant the final access_token. If
255    *     set, the sequence of identities must have "Service Account Token Creator" capability
256    *     granted to the preceding identity. For example, if set to [serviceAccountB,
257    *     serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
258    *     serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
259    *     Creator on target_principal. If left unset, sourceCredential must have that role on
260    *     targetPrincipal.
261    * @param scopes scopes to request during the authorization grant
262    * @param lifetime number of seconds the delegated credential should be valid. By default this
263    *     value should be at most 3600. However, you can follow <a
264    *     href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
265    *     instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
266    *     hours).
267    *     https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth
268    *     If the given lifetime is 0, default value 3600 will be used instead when creating the
269    *     credentials.
270    * @return new credentials
271    */
create( GoogleCredentials sourceCredentials, String targetPrincipal, List<String> delegates, List<String> scopes, int lifetime)272   public static ImpersonatedCredentials create(
273       GoogleCredentials sourceCredentials,
274       String targetPrincipal,
275       List<String> delegates,
276       List<String> scopes,
277       int lifetime) {
278     return ImpersonatedCredentials.newBuilder()
279         .setSourceCredentials(sourceCredentials)
280         .setTargetPrincipal(targetPrincipal)
281         .setDelegates(delegates)
282         .setScopes(scopes)
283         .setLifetime(lifetime)
284         .build();
285   }
286 
extractTargetPrincipal(String serviceAccountImpersonationUrl)287   static String extractTargetPrincipal(String serviceAccountImpersonationUrl) {
288     // Extract the target principal.
289     int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/');
290     int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken");
291 
292     if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
293       return serviceAccountImpersonationUrl.substring(startIndex + 1, endIndex);
294     } else {
295       throw new IllegalArgumentException(
296           "Unable to determine target principal from service account impersonation URL.");
297     }
298   }
299 
300   /**
301    * Returns the email field of the serviceAccount that is being impersonated.
302    *
303    * @return email address of the impersonated service account
304    */
305   @Override
getAccount()306   public String getAccount() {
307     return this.targetPrincipal;
308   }
309 
310   @VisibleForTesting
getIamEndpointOverride()311   String getIamEndpointOverride() {
312     return this.iamEndpointOverride;
313   }
314 
315   @VisibleForTesting
getDelegates()316   List<String> getDelegates() {
317     return delegates;
318   }
319 
320   @VisibleForTesting
getScopes()321   List<String> getScopes() {
322     return scopes;
323   }
324 
getSourceCredentials()325   public GoogleCredentials getSourceCredentials() {
326     return sourceCredentials;
327   }
328 
getLifetime()329   int getLifetime() {
330     return this.lifetime;
331   }
332 
setTransportFactory(HttpTransportFactory httpTransportFactory)333   public void setTransportFactory(HttpTransportFactory httpTransportFactory) {
334     this.transportFactory = httpTransportFactory;
335   }
336 
337   /**
338    * Signs the provided bytes using the private key associated with the impersonated service account
339    *
340    * @param toSign bytes to sign
341    * @return signed bytes
342    * @throws SigningException if the attempt to sign the provided bytes failed
343    * @see <a
344    *     href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob
345    *     Signing</a>
346    */
347   @Override
sign(byte[] toSign)348   public byte[] sign(byte[] toSign) {
349     return IamUtils.sign(
350         getAccount(),
351         sourceCredentials,
352         transportFactory.create(),
353         toSign,
354         ImmutableMap.of("delegates", this.delegates));
355   }
356 
357   /**
358    * Returns impersonation account credentials defined by JSON using the format generated by gCloud.
359    * The source credentials in the JSON should be either user account credentials or service account
360    * credentials.
361    *
362    * @param json a map from the JSON representing the credentials
363    * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
364    * @return the credentials defined by the JSON
365    * @throws IOException if the credential cannot be created from the JSON.
366    */
367   @SuppressWarnings("unchecked")
fromJson( Map<String, Object> json, HttpTransportFactory transportFactory)368   static ImpersonatedCredentials fromJson(
369       Map<String, Object> json, HttpTransportFactory transportFactory) throws IOException {
370 
371     checkNotNull(json);
372     checkNotNull(transportFactory);
373 
374     List<String> delegates = null;
375     Map<String, Object> sourceCredentialsJson;
376     String sourceCredentialsType;
377     String quotaProjectId;
378     String targetPrincipal;
379     String serviceAccountImpersonationUrl;
380     try {
381       serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
382       if (json.containsKey("delegates")) {
383         delegates = (List<String>) json.get("delegates");
384       }
385       sourceCredentialsJson = (Map<String, Object>) json.get("source_credentials");
386       sourceCredentialsType = (String) sourceCredentialsJson.get("type");
387       quotaProjectId = (String) json.get("quota_project_id");
388       targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
389     } catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
390       throw new CredentialFormatException("An invalid input stream was provided.", e);
391     }
392 
393     GoogleCredentials sourceCredentials;
394     if (GoogleCredentials.USER_FILE_TYPE.equals(sourceCredentialsType)) {
395       sourceCredentials = UserCredentials.fromJson(sourceCredentialsJson, transportFactory);
396     } else if (GoogleCredentials.SERVICE_ACCOUNT_FILE_TYPE.equals(sourceCredentialsType)) {
397       sourceCredentials =
398           ServiceAccountCredentials.fromJson(sourceCredentialsJson, transportFactory);
399     } else {
400       throw new IOException(
401           String.format(
402               "A credential of type %s is not supported as source credential for impersonation.",
403               sourceCredentialsType));
404     }
405     return ImpersonatedCredentials.newBuilder()
406         .setSourceCredentials(sourceCredentials)
407         .setTargetPrincipal(targetPrincipal)
408         .setDelegates(delegates)
409         .setScopes(new ArrayList<String>())
410         .setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
411         .setHttpTransportFactory(transportFactory)
412         .setQuotaProjectId(quotaProjectId)
413         .setIamEndpointOverride(serviceAccountImpersonationUrl)
414         .build();
415   }
416 
417   @Override
createScopedRequired()418   public boolean createScopedRequired() {
419     return this.scopes == null || this.scopes.isEmpty();
420   }
421 
422   @Override
createScoped(Collection<String> scopes)423   public GoogleCredentials createScoped(Collection<String> scopes) {
424     return toBuilder()
425         .setScopes(new ArrayList<>(scopes))
426         .setLifetime(this.lifetime)
427         .setDelegates(this.delegates)
428         .setHttpTransportFactory(this.transportFactory)
429         .setQuotaProjectId(this.quotaProjectId)
430         .setIamEndpointOverride(this.iamEndpointOverride)
431         .build();
432   }
433 
434   /**
435    * Clones the impersonated credentials with a new calendar.
436    *
437    * @param calendar the calendar that will be used by the new ImpersonatedCredentials instance when
438    *     parsing the received expiration time of the refreshed access token
439    * @return the cloned impersonated credentials with the given custom calendar
440    */
createWithCustomCalendar(Calendar calendar)441   public ImpersonatedCredentials createWithCustomCalendar(Calendar calendar) {
442     return toBuilder()
443         .setScopes(this.scopes)
444         .setLifetime(this.lifetime)
445         .setDelegates(this.delegates)
446         .setHttpTransportFactory(this.transportFactory)
447         .setQuotaProjectId(this.quotaProjectId)
448         .setIamEndpointOverride(this.iamEndpointOverride)
449         .setCalendar(calendar)
450         .build();
451   }
452 
ImpersonatedCredentials(Builder builder)453   private ImpersonatedCredentials(Builder builder) {
454     super(builder);
455     this.sourceCredentials = builder.getSourceCredentials();
456     this.targetPrincipal = builder.getTargetPrincipal();
457     this.delegates = builder.getDelegates();
458     this.scopes = builder.getScopes();
459     this.lifetime = builder.getLifetime();
460     this.transportFactory =
461         firstNonNull(
462             builder.getHttpTransportFactory(),
463             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
464     this.iamEndpointOverride = builder.iamEndpointOverride;
465     this.transportFactoryClassName = this.transportFactory.getClass().getName();
466     this.calendar = builder.getCalendar();
467     if (this.delegates == null) {
468       this.delegates = new ArrayList<String>();
469     }
470     if (this.scopes == null) {
471       throw new IllegalStateException("Scopes cannot be null");
472     }
473     if (this.lifetime > TWELVE_HOURS_IN_SECONDS) {
474       throw new IllegalStateException("lifetime must be less than or equal to 43200");
475     }
476   }
477 
478   @Override
refreshAccessToken()479   public AccessToken refreshAccessToken() throws IOException {
480     if (this.sourceCredentials.getAccessToken() == null) {
481       this.sourceCredentials =
482           this.sourceCredentials.createScoped(Arrays.asList(CLOUD_PLATFORM_SCOPE));
483     }
484 
485     try {
486       this.sourceCredentials.refreshIfExpired();
487     } catch (IOException e) {
488       throw new IOException("Unable to refresh sourceCredentials", e);
489     }
490 
491     HttpTransport httpTransport = this.transportFactory.create();
492     JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
493 
494     HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
495     HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
496 
497     String endpointUrl =
498         this.iamEndpointOverride != null
499             ? this.iamEndpointOverride
500             : String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
501     GenericUrl url = new GenericUrl(endpointUrl);
502 
503     Map<String, Object> body =
504         ImmutableMap.<String, Object>of(
505             "delegates", this.delegates, "scope", this.scopes, "lifetime", this.lifetime + "s");
506 
507     HttpContent requestContent = new JsonHttpContent(parser.getJsonFactory(), body);
508     HttpRequest request = requestFactory.buildPostRequest(url, requestContent);
509     adapter.initialize(request);
510     request.setParser(parser);
511 
512     HttpResponse response = null;
513     try {
514       response = request.execute();
515     } catch (IOException e) {
516       throw new IOException("Error requesting access token", e);
517     }
518 
519     GenericData responseData = response.parseAs(GenericData.class);
520     response.disconnect();
521 
522     String accessToken =
523         OAuth2Utils.validateString(responseData, "accessToken", "Expected to find an accessToken");
524     String expireTime =
525         OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");
526 
527     DateFormat format = new SimpleDateFormat(RFC3339);
528     format.setCalendar(calendar);
529     try {
530       Date date = format.parse(expireTime);
531       return new AccessToken(accessToken, date);
532     } catch (ParseException pe) {
533       throw new IOException("Error parsing expireTime: " + pe.getMessage());
534     }
535   }
536 
537   /**
538    * Returns an IdToken for the current Credential.
539    *
540    * @param targetAudience the audience field for the issued ID token
541    * @param options credential specific options for for the token. For example, an ID token for an
542    *     ImpersonatedCredentials can return the email address within the token claims if
543    *     "ImpersonatedCredentials.INCLUDE_EMAIL" is provided as a list option.<br>
544    *     Only one option value is supported: "ImpersonatedCredentials.INCLUDE_EMAIL" If no options
545    *     are set, the default excludes the "includeEmail" attribute in the API request.
546    * @return IdToken object which includes the raw id_token, expiration, and audience
547    * @throws IOException if the attempt to get an ID token failed
548    */
549   @Override
idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)550   public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)
551       throws IOException {
552     boolean includeEmail =
553         options != null && options.contains(IdTokenProvider.Option.INCLUDE_EMAIL);
554     return IamUtils.getIdToken(
555         getAccount(),
556         sourceCredentials,
557         transportFactory.create(),
558         targetAudience,
559         includeEmail,
560         ImmutableMap.of("delegates", this.delegates));
561   }
562 
563   @Override
hashCode()564   public int hashCode() {
565     return Objects.hash(
566         sourceCredentials,
567         targetPrincipal,
568         delegates,
569         scopes,
570         lifetime,
571         quotaProjectId,
572         iamEndpointOverride);
573   }
574 
575   @Override
toString()576   public String toString() {
577     return MoreObjects.toStringHelper(this)
578         .add("sourceCredentials", sourceCredentials)
579         .add("targetPrincipal", targetPrincipal)
580         .add("delegates", delegates)
581         .add("scopes", scopes)
582         .add("lifetime", lifetime)
583         .add("transportFactoryClassName", transportFactoryClassName)
584         .add("quotaProjectId", quotaProjectId)
585         .add("iamEndpointOverride", iamEndpointOverride)
586         .toString();
587   }
588 
589   @Override
equals(Object obj)590   public boolean equals(Object obj) {
591     if (!(obj instanceof ImpersonatedCredentials)) {
592       return false;
593     }
594     ImpersonatedCredentials other = (ImpersonatedCredentials) obj;
595     return Objects.equals(this.sourceCredentials, other.sourceCredentials)
596         && Objects.equals(this.targetPrincipal, other.targetPrincipal)
597         && Objects.equals(this.delegates, other.delegates)
598         && Objects.equals(this.scopes, other.scopes)
599         && Objects.equals(this.lifetime, other.lifetime)
600         && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
601         && Objects.equals(this.quotaProjectId, other.quotaProjectId)
602         && Objects.equals(this.iamEndpointOverride, other.iamEndpointOverride);
603   }
604 
605   @Override
toBuilder()606   public Builder toBuilder() {
607     return new Builder(this.sourceCredentials, this.targetPrincipal);
608   }
609 
newBuilder()610   public static Builder newBuilder() {
611     return new Builder();
612   }
613 
614   public static class Builder extends GoogleCredentials.Builder {
615 
616     private GoogleCredentials sourceCredentials;
617     private String targetPrincipal;
618     private List<String> delegates;
619     private List<String> scopes;
620     private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
621     private HttpTransportFactory transportFactory;
622     private String iamEndpointOverride;
623     private Calendar calendar = Calendar.getInstance();
624 
Builder()625     protected Builder() {}
626 
Builder(GoogleCredentials sourceCredentials, String targetPrincipal)627     protected Builder(GoogleCredentials sourceCredentials, String targetPrincipal) {
628       this.sourceCredentials = sourceCredentials;
629       this.targetPrincipal = targetPrincipal;
630     }
631 
632     @CanIgnoreReturnValue
setSourceCredentials(GoogleCredentials sourceCredentials)633     public Builder setSourceCredentials(GoogleCredentials sourceCredentials) {
634       this.sourceCredentials = sourceCredentials;
635       return this;
636     }
637 
getSourceCredentials()638     public GoogleCredentials getSourceCredentials() {
639       return this.sourceCredentials;
640     }
641 
642     @CanIgnoreReturnValue
setTargetPrincipal(String targetPrincipal)643     public Builder setTargetPrincipal(String targetPrincipal) {
644       this.targetPrincipal = targetPrincipal;
645       return this;
646     }
647 
getTargetPrincipal()648     public String getTargetPrincipal() {
649       return this.targetPrincipal;
650     }
651 
652     @CanIgnoreReturnValue
setDelegates(List<String> delegates)653     public Builder setDelegates(List<String> delegates) {
654       this.delegates = delegates;
655       return this;
656     }
657 
getDelegates()658     public List<String> getDelegates() {
659       return this.delegates;
660     }
661 
662     @CanIgnoreReturnValue
setScopes(List<String> scopes)663     public Builder setScopes(List<String> scopes) {
664       this.scopes = scopes;
665       return this;
666     }
667 
getScopes()668     public List<String> getScopes() {
669       return this.scopes;
670     }
671 
672     @CanIgnoreReturnValue
setLifetime(int lifetime)673     public Builder setLifetime(int lifetime) {
674       this.lifetime = lifetime == 0 ? DEFAULT_LIFETIME_IN_SECONDS : lifetime;
675       return this;
676     }
677 
getLifetime()678     public int getLifetime() {
679       return this.lifetime;
680     }
681 
682     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)683     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
684       this.transportFactory = transportFactory;
685       return this;
686     }
687 
getHttpTransportFactory()688     public HttpTransportFactory getHttpTransportFactory() {
689       return transportFactory;
690     }
691 
692     @Override
693     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)694     public Builder setQuotaProjectId(String quotaProjectId) {
695       super.setQuotaProjectId(quotaProjectId);
696       return this;
697     }
698 
699     @CanIgnoreReturnValue
setIamEndpointOverride(String iamEndpointOverride)700     public Builder setIamEndpointOverride(String iamEndpointOverride) {
701       this.iamEndpointOverride = iamEndpointOverride;
702       return this;
703     }
704 
705     @CanIgnoreReturnValue
setCalendar(Calendar calendar)706     public Builder setCalendar(Calendar calendar) {
707       this.calendar = calendar;
708       return this;
709     }
710 
getCalendar()711     public Calendar getCalendar() {
712       return this.calendar;
713     }
714 
715     @Override
build()716     public ImpersonatedCredentials build() {
717       return new ImpersonatedCredentials(this);
718     }
719   }
720 
readObject(ObjectInputStream input)721   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
722     input.defaultReadObject();
723     transportFactory = newInstance(transportFactoryClassName);
724   }
725 }
726