• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
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 LLC 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.OAuth2Utils.JSON_FACTORY;
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.HttpHeaders;
39 import com.google.api.client.http.HttpRequest;
40 import com.google.api.client.http.HttpResponse;
41 import com.google.api.client.http.HttpResponseException;
42 import com.google.api.client.http.UrlEncodedContent;
43 import com.google.api.client.json.GenericJson;
44 import com.google.api.client.json.JsonObjectParser;
45 import com.google.api.client.util.GenericData;
46 import com.google.api.client.util.Preconditions;
47 import com.google.auth.http.HttpTransportFactory;
48 import com.google.common.base.MoreObjects;
49 import com.google.common.io.BaseEncoding;
50 import com.google.errorprone.annotations.CanIgnoreReturnValue;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.io.ObjectInputStream;
54 import java.nio.charset.StandardCharsets;
55 import java.util.Date;
56 import java.util.Map;
57 import java.util.Objects;
58 import javax.annotation.Nullable;
59 
60 /**
61  * OAuth2 credentials sourced using external identities through Workforce Identity Federation.
62  *
63  * <p>Obtaining the initial access and refresh token can be done through the Google Cloud CLI.
64  *
65  * <pre>
66  * Example credentials file:
67  * {
68  *   "type": "external_account_authorized_user",
69  *   "audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
70  *   "refresh_token": "refreshToken",
71  *   "token_url": "https://sts.googleapis.com/v1/oauthtoken",
72  *   "token_info_url": "https://sts.googleapis.com/v1/introspect",
73  *   "client_id": "clientId",
74  *   "client_secret": "clientSecret"
75  * }
76  * </pre>
77  */
78 public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
79 
80   private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
81 
82   private static final long serialVersionUID = -2181779590486283287L;
83 
84   static final String EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE_TYPE =
85       "external_account_authorized_user";
86 
87   private final String transportFactoryClassName;
88   private final String audience;
89   private final String tokenUrl;
90   private final String tokenInfoUrl;
91   private final String revokeUrl;
92   private final String clientId;
93   private final String clientSecret;
94 
95   private String refreshToken;
96 
97   private transient HttpTransportFactory transportFactory;
98 
99   /**
100    * Internal constructor.
101    *
102    * @param builder A builder for {@link ExternalAccountAuthorizedUserCredentials}. See {@link
103    *     ExternalAccountAuthorizedUserCredentials.Builder}
104    */
ExternalAccountAuthorizedUserCredentials(Builder builder)105   private ExternalAccountAuthorizedUserCredentials(Builder builder) {
106     super(builder);
107     this.transportFactory =
108         MoreObjects.firstNonNull(
109             builder.transportFactory,
110             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
111     this.transportFactoryClassName = this.transportFactory.getClass().getName();
112     this.audience = builder.audience;
113     this.refreshToken = builder.refreshToken;
114     this.tokenUrl = builder.tokenUrl;
115     this.tokenInfoUrl = builder.tokenInfoUrl;
116     this.revokeUrl = builder.revokeUrl;
117     this.clientId = builder.clientId;
118     this.clientSecret = builder.clientSecret;
119 
120     Preconditions.checkState(
121         getAccessToken() != null || canRefresh(),
122         "ExternalAccountAuthorizedUserCredentials must be initialized with "
123             + "an access token or fields to enable refresh: "
124             + "('refresh_token', 'token_url', 'client_id', 'client_secret').");
125   }
126 
127   /**
128    * Returns external account authorized user credentials defined by a JSON file stream.
129    *
130    * @param credentialsStream the stream with the credential definition
131    * @return the credential defined by the credentialsStream
132    * @throws IOException if the credential cannot be created from the stream
133    */
fromStream(InputStream credentialsStream)134   public static ExternalAccountAuthorizedUserCredentials fromStream(InputStream credentialsStream)
135       throws IOException {
136     checkNotNull(credentialsStream);
137     return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
138   }
139 
140   /**
141    * Returns external account authorized user credentials defined by a JSON file stream.
142    *
143    * @param credentialsStream the stream with the credential definition
144    * @param transportFactory the HTTP transport factory used to create the transport to get access
145    *     tokens
146    * @return the credential defined by the credentialsStream
147    * @throws IOException if the credential cannot be created from the stream
148    */
fromStream( InputStream credentialsStream, HttpTransportFactory transportFactory)149   public static ExternalAccountAuthorizedUserCredentials fromStream(
150       InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException {
151     checkNotNull(credentialsStream);
152     checkNotNull(transportFactory);
153 
154     JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
155     GenericJson fileContents =
156         parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
157     try {
158       return fromJson(fileContents, transportFactory);
159     } catch (ClassCastException | IllegalArgumentException e) {
160       throw new CredentialFormatException("Invalid input stream provided.", e);
161     }
162   }
163 
164   @Override
refreshAccessToken()165   public AccessToken refreshAccessToken() throws IOException {
166     if (!canRefresh()) {
167       throw new IllegalStateException(
168           "Unable to refresh ExternalAccountAuthorizedUserCredentials. All of 'refresh_token',"
169               + "'token_url', 'client_id', 'client_secret' are required to refresh.");
170     }
171 
172     HttpResponse response;
173     try {
174       HttpRequest httpRequest = buildRefreshRequest();
175       response = httpRequest.execute();
176     } catch (HttpResponseException e) {
177       throw OAuthException.createFromHttpResponseException(e);
178     }
179 
180     // Parse response.
181     GenericData responseData = response.parseAs(GenericData.class);
182     response.disconnect();
183 
184     // Required fields.
185     String accessToken =
186         OAuth2Utils.validateString(responseData, /* key= */ "access_token", PARSE_ERROR_PREFIX);
187     int expiresInSeconds =
188         OAuth2Utils.validateInt32(responseData, /* key= */ "expires_in", PARSE_ERROR_PREFIX);
189     Date expiresAtMilliseconds = new Date(clock.currentTimeMillis() + expiresInSeconds * 1000L);
190 
191     // Set the new refresh token if returned.
192     String refreshToken =
193         OAuth2Utils.validateOptionalString(
194             responseData, /* key= */ "refresh_token", PARSE_ERROR_PREFIX);
195     if (refreshToken != null && refreshToken.trim().length() > 0) {
196       this.refreshToken = refreshToken;
197     }
198 
199     return AccessToken.newBuilder()
200         .setExpirationTime(expiresAtMilliseconds)
201         .setTokenValue(accessToken)
202         .build();
203   }
204 
205   @Nullable
getAudience()206   public String getAudience() {
207     return audience;
208   }
209 
210   @Nullable
getClientId()211   public String getClientId() {
212     return clientId;
213   }
214 
215   @Nullable
getClientSecret()216   public String getClientSecret() {
217     return clientSecret;
218   }
219 
220   @Nullable
getRevokeUrl()221   public String getRevokeUrl() {
222     return revokeUrl;
223   }
224 
225   @Nullable
getTokenUrl()226   public String getTokenUrl() {
227     return tokenUrl;
228   }
229 
230   @Nullable
getTokenInfoUrl()231   public String getTokenInfoUrl() {
232     return tokenInfoUrl;
233   }
234 
235   @Nullable
getRefreshToken()236   public String getRefreshToken() {
237     return refreshToken;
238   }
239 
newBuilder()240   public static Builder newBuilder() {
241     return new Builder();
242   }
243 
244   @Override
hashCode()245   public int hashCode() {
246     return Objects.hash(
247         super.hashCode(),
248         getAccessToken(),
249         clientId,
250         clientSecret,
251         refreshToken,
252         tokenUrl,
253         tokenInfoUrl,
254         revokeUrl,
255         audience,
256         transportFactoryClassName,
257         quotaProjectId);
258   }
259 
260   @Override
toString()261   public String toString() {
262     return MoreObjects.toStringHelper(this)
263         .add("requestMetadata", getRequestMetadataInternal())
264         .add("temporaryAccess", getAccessToken())
265         .add("clientId", clientId)
266         .add("clientSecret", clientSecret)
267         .add("refreshToken", refreshToken)
268         .add("tokenUrl", tokenUrl)
269         .add("tokenInfoUrl", tokenInfoUrl)
270         .add("revokeUrl", revokeUrl)
271         .add("audience", audience)
272         .add("transportFactoryClassName", transportFactoryClassName)
273         .add("quotaProjectId", quotaProjectId)
274         .toString();
275   }
276 
277   @Override
equals(Object obj)278   public boolean equals(Object obj) {
279     if (!(obj instanceof ExternalAccountAuthorizedUserCredentials)) {
280       return false;
281     }
282     ExternalAccountAuthorizedUserCredentials credentials =
283         (ExternalAccountAuthorizedUserCredentials) obj;
284     return super.equals(credentials)
285         && Objects.equals(this.getAccessToken(), credentials.getAccessToken())
286         && Objects.equals(this.clientId, credentials.clientId)
287         && Objects.equals(this.clientSecret, credentials.clientSecret)
288         && Objects.equals(this.refreshToken, credentials.refreshToken)
289         && Objects.equals(this.tokenUrl, credentials.tokenUrl)
290         && Objects.equals(this.tokenInfoUrl, credentials.tokenInfoUrl)
291         && Objects.equals(this.revokeUrl, credentials.revokeUrl)
292         && Objects.equals(this.audience, credentials.audience)
293         && Objects.equals(this.transportFactoryClassName, credentials.transportFactoryClassName)
294         && Objects.equals(this.quotaProjectId, credentials.quotaProjectId);
295   }
296 
297   @Override
toBuilder()298   public Builder toBuilder() {
299     return new Builder(this);
300   }
301 
302   /**
303    * Returns external account authorized user credentials defined by JSON contents using the format
304    * supported by the Cloud SDK.
305    *
306    * @param json a map from the JSON representing the credentials
307    * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
308    * @return the external account authorized user credentials defined by the JSON
309    */
fromJson( Map<String, Object> json, HttpTransportFactory transportFactory)310   static ExternalAccountAuthorizedUserCredentials fromJson(
311       Map<String, Object> json, HttpTransportFactory transportFactory) throws IOException {
312     String audience = (String) json.get("audience");
313     String refreshToken = (String) json.get("refresh_token");
314     String tokenUrl = (String) json.get("token_url");
315     String tokenInfoUrl = (String) json.get("token_info_url");
316     String revokeUrl = (String) json.get("revoke_url");
317     String clientId = (String) json.get("client_id");
318     String clientSecret = (String) json.get("client_secret");
319     String quotaProjectId = (String) json.get("quota_project_id");
320     String universeDomain = (String) json.get("universe_domain");
321 
322     return ExternalAccountAuthorizedUserCredentials.newBuilder()
323         .setAudience(audience)
324         .setRefreshToken(refreshToken)
325         .setTokenUrl(tokenUrl)
326         .setTokenInfoUrl(tokenInfoUrl)
327         .setRevokeUrl(revokeUrl)
328         .setClientId(clientId)
329         .setClientSecret(clientSecret)
330         .setRefreshToken(refreshToken)
331         .setHttpTransportFactory(transportFactory)
332         .setQuotaProjectId(quotaProjectId)
333         .setUniverseDomain(universeDomain)
334         .build();
335   }
336 
readObject(ObjectInputStream input)337   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
338     input.defaultReadObject();
339     transportFactory = newInstance(transportFactoryClassName);
340   }
341 
canRefresh()342   private boolean canRefresh() {
343     return refreshToken != null
344         && refreshToken.trim().length() > 0
345         && tokenUrl != null
346         && tokenUrl.trim().length() > 0
347         && clientId != null
348         && clientId.trim().length() > 0
349         && clientSecret != null
350         && clientSecret.trim().length() > 0;
351   }
352 
buildRefreshRequest()353   private HttpRequest buildRefreshRequest() throws IOException {
354     GenericData tokenRequest = new GenericData();
355     tokenRequest.set("grant_type", "refresh_token");
356     tokenRequest.set("refresh_token", refreshToken);
357 
358     HttpRequest request =
359         transportFactory
360             .create()
361             .createRequestFactory()
362             .buildPostRequest(new GenericUrl(tokenUrl), new UrlEncodedContent(tokenRequest));
363 
364     request.setParser(new JsonObjectParser(JSON_FACTORY));
365 
366     HttpHeaders requestHeaders = request.getHeaders();
367     requestHeaders.setAuthorization(
368         String.format(
369             "Basic %s",
370             BaseEncoding.base64()
371                 .encode(
372                     String.format("%s:%s", clientId, clientSecret)
373                         .getBytes(StandardCharsets.UTF_8))));
374 
375     return request;
376   }
377 
378   /** Builder for {@link ExternalAccountAuthorizedUserCredentials}. */
379   public static class Builder extends GoogleCredentials.Builder {
380 
381     private HttpTransportFactory transportFactory;
382     private String audience;
383     private String refreshToken;
384     private String tokenUrl;
385     private String tokenInfoUrl;
386     private String revokeUrl;
387     private String clientId;
388     private String clientSecret;
389 
Builder()390     protected Builder() {}
391 
Builder(ExternalAccountAuthorizedUserCredentials credentials)392     protected Builder(ExternalAccountAuthorizedUserCredentials credentials) {
393       super(credentials);
394       this.transportFactory = credentials.transportFactory;
395       this.audience = credentials.audience;
396       this.refreshToken = credentials.refreshToken;
397       this.tokenUrl = credentials.tokenUrl;
398       this.tokenInfoUrl = credentials.tokenInfoUrl;
399       this.revokeUrl = credentials.revokeUrl;
400       this.clientId = credentials.clientId;
401       this.clientSecret = credentials.clientSecret;
402     }
403 
404     /**
405      * Sets the HTTP transport factory.
406      *
407      * @param transportFactory the {@code HttpTransportFactory} to set
408      * @return this {@code Builder} object
409      */
410     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)411     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
412       this.transportFactory = transportFactory;
413       return this;
414     }
415 
416     /**
417      * Sets the optional audience, which is usually the fully specified resource name of the
418      * workforce pool provider.
419      *
420      * @param audience the audience to set
421      * @return this {@code Builder} object
422      */
423     @CanIgnoreReturnValue
setAudience(String audience)424     public Builder setAudience(String audience) {
425       this.audience = audience;
426       return this;
427     }
428 
429     /**
430      * Sets the token exchange endpoint.
431      *
432      * @param tokenUrl the token exchange url to set
433      * @return this {@code Builder} object
434      */
435     @CanIgnoreReturnValue
setTokenUrl(String tokenUrl)436     public Builder setTokenUrl(String tokenUrl) {
437       this.tokenUrl = tokenUrl;
438       return this;
439     }
440 
441     /**
442      * Sets the token introspection endpoint used to retrieve account related information.
443      *
444      * @param tokenInfoUrl the token info url to set
445      * @return this {@code Builder} object
446      */
447     @CanIgnoreReturnValue
setTokenInfoUrl(String tokenInfoUrl)448     public Builder setTokenInfoUrl(String tokenInfoUrl) {
449       this.tokenInfoUrl = tokenInfoUrl;
450       return this;
451     }
452 
453     /**
454      * Sets the token revocation endpoint.
455      *
456      * @param revokeUrl the revoke url to set
457      * @return this {@code Builder} object
458      */
459     @CanIgnoreReturnValue
setRevokeUrl(String revokeUrl)460     public Builder setRevokeUrl(String revokeUrl) {
461       this.revokeUrl = revokeUrl;
462       return this;
463     }
464 
465     /**
466      * Sets the OAuth 2.0 refresh token.
467      *
468      * @param refreshToken the refresh token
469      * @return this {@code Builder} object
470      */
471     @CanIgnoreReturnValue
setRefreshToken(String refreshToken)472     public Builder setRefreshToken(String refreshToken) {
473       this.refreshToken = refreshToken;
474       return this;
475     }
476 
477     /**
478      * Sets the OAuth 2.0 client ID.
479      *
480      * @param clientId the client ID
481      * @return this {@code Builder} object
482      */
483     @CanIgnoreReturnValue
setClientId(String clientId)484     public Builder setClientId(String clientId) {
485       this.clientId = clientId;
486       return this;
487     }
488 
489     /**
490      * Sets the OAuth 2.0 client secret.
491      *
492      * @param clientSecret the client secret
493      * @return this {@code Builder} object
494      */
495     @CanIgnoreReturnValue
setClientSecret(String clientSecret)496     public Builder setClientSecret(String clientSecret) {
497       this.clientSecret = clientSecret;
498       return this;
499     }
500 
501     /**
502      * Sets the optional project used for quota and billing purposes.
503      *
504      * @param quotaProjectId the quota and billing project id to set
505      * @return this {@code Builder} object
506      */
507     @Override
508     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)509     public Builder setQuotaProjectId(String quotaProjectId) {
510       super.setQuotaProjectId(quotaProjectId);
511       return this;
512     }
513 
514     /**
515      * Sets the optional access token.
516      *
517      * @param accessToken the access token
518      * @return this {@code Builder} object
519      */
520     @Override
521     @CanIgnoreReturnValue
setAccessToken(AccessToken accessToken)522     public Builder setAccessToken(AccessToken accessToken) {
523       super.setAccessToken(accessToken);
524       return this;
525     }
526 
527     /**
528      * Sets the optional universe domain. The Google Default Universe is used when not provided.
529      *
530      * @param universeDomain the universe domain to set
531      * @return this {@code Builder} object
532      */
533     @CanIgnoreReturnValue
534     @Override
setUniverseDomain(String universeDomain)535     public Builder setUniverseDomain(String universeDomain) {
536       super.setUniverseDomain(universeDomain);
537       return this;
538     }
539 
540     @Override
build()541     public ExternalAccountAuthorizedUserCredentials build() {
542       return new ExternalAccountAuthorizedUserCredentials(this);
543     }
544   }
545 }
546