• 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.OAuth2Utils.JSON_FACTORY;
35 import static com.google.common.base.MoreObjects.firstNonNull;
36 
37 import com.google.api.client.http.GenericUrl;
38 import com.google.api.client.http.HttpRequest;
39 import com.google.api.client.http.HttpRequestFactory;
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.JsonFactory;
45 import com.google.api.client.json.JsonObjectParser;
46 import com.google.api.client.util.GenericData;
47 import com.google.api.client.util.Preconditions;
48 import com.google.auth.http.HttpTransportFactory;
49 import com.google.common.base.MoreObjects;
50 import com.google.errorprone.annotations.CanIgnoreReturnValue;
51 import java.io.ByteArrayInputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.ObjectInputStream;
55 import java.net.URI;
56 import java.nio.charset.StandardCharsets;
57 import java.time.Duration;
58 import java.util.Date;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Objects;
62 
63 /** OAuth2 Credentials representing a user's identity and consent. */
64 public class UserCredentials extends GoogleCredentials implements IdTokenProvider {
65 
66   private static final String GRANT_TYPE = "refresh_token";
67   private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
68   private static final long serialVersionUID = -4800758775038679176L;
69 
70   private final String clientId;
71   private final String clientSecret;
72   private final String refreshToken;
73   private final URI tokenServerUri;
74   private final String transportFactoryClassName;
75 
76   private transient HttpTransportFactory transportFactory;
77 
78   /**
79    * Internal constructor
80    *
81    * @param builder A builder for {@link UserCredentials} See {@link UserCredentials.Builder}
82    */
UserCredentials(Builder builder)83   private UserCredentials(Builder builder) {
84     super(builder);
85     this.clientId = Preconditions.checkNotNull(builder.clientId);
86     this.clientSecret = Preconditions.checkNotNull(builder.clientSecret);
87     this.refreshToken = builder.refreshToken;
88     this.transportFactory =
89         firstNonNull(
90             builder.transportFactory,
91             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
92     this.tokenServerUri =
93         (builder.tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : builder.tokenServerUri;
94     this.transportFactoryClassName = this.transportFactory.getClass().getName();
95     Preconditions.checkState(
96         builder.getAccessToken() != null || builder.refreshToken != null,
97         "Either accessToken or refreshToken must not be null");
98   }
99 
100   /**
101    * Returns user credentials defined by JSON contents using the format supported by the Cloud SDK.
102    *
103    * @param json a map from the JSON representing the credentials.
104    * @param transportFactory HTTP transport factory, creates the transport used to get access
105    *     tokens.
106    * @return the credentials defined by the JSON.
107    * @throws IOException if the credential cannot be created from the JSON.
108    */
fromJson(Map<String, Object> json, HttpTransportFactory transportFactory)109   static UserCredentials fromJson(Map<String, Object> json, HttpTransportFactory transportFactory)
110       throws IOException {
111     String clientId = (String) json.get("client_id");
112     String clientSecret = (String) json.get("client_secret");
113     String refreshToken = (String) json.get("refresh_token");
114     String quotaProjectId = (String) json.get("quota_project_id");
115     if (clientId == null || clientSecret == null || refreshToken == null) {
116       throw new IOException(
117           "Error reading user credential from JSON, "
118               + " expecting 'client_id', 'client_secret' and 'refresh_token'.");
119     }
120     return UserCredentials.newBuilder()
121         .setClientId(clientId)
122         .setClientSecret(clientSecret)
123         .setRefreshToken(refreshToken)
124         .setAccessToken(null)
125         .setHttpTransportFactory(transportFactory)
126         .setTokenServerUri(null)
127         .setQuotaProjectId(quotaProjectId)
128         .build();
129   }
130 
131   /**
132    * Returns credentials defined by a JSON file stream using the format supported by the Cloud SDK.
133    *
134    * @param credentialsStream the stream with the credential definition.
135    * @return the credential defined by the credentialsStream.
136    * @throws IOException if the credential cannot be created from the stream.
137    */
fromStream(InputStream credentialsStream)138   public static UserCredentials fromStream(InputStream credentialsStream) throws IOException {
139     return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
140   }
141 
142   /**
143    * Returns credentials defined by a JSON file stream using the format supported by the Cloud SDK.
144    *
145    * @param credentialsStream the stream with the credential definition.
146    * @param transportFactory HTTP transport factory, creates the transport used to get access
147    *     tokens.
148    * @return the credential defined by the credentialsStream.
149    * @throws IOException if the credential cannot be created from the stream.
150    */
fromStream( InputStream credentialsStream, HttpTransportFactory transportFactory)151   public static UserCredentials fromStream(
152       InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException {
153     Preconditions.checkNotNull(credentialsStream);
154     Preconditions.checkNotNull(transportFactory);
155 
156     JsonFactory jsonFactory = JSON_FACTORY;
157     JsonObjectParser parser = new JsonObjectParser(jsonFactory);
158     GenericJson fileContents =
159         parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
160 
161     String fileType = (String) fileContents.get("type");
162     if (fileType == null) {
163       throw new IOException("Error reading credentials from stream, 'type' field not specified.");
164     }
165     if (USER_FILE_TYPE.equals(fileType)) {
166       return fromJson(fileContents, transportFactory);
167     }
168     throw new IOException(
169         String.format(
170             "Error reading credentials from stream, 'type' value '%s' not recognized."
171                 + " Expecting '%s'.",
172             fileType, USER_FILE_TYPE));
173   }
174 
175   /** Refreshes the OAuth2 access token by getting a new access token from the refresh token */
176   @Override
refreshAccessToken()177   public AccessToken refreshAccessToken() throws IOException {
178     GenericData responseData = doRefreshAccessToken();
179     String accessToken =
180         OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX);
181     int expiresInSeconds =
182         OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
183     long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
184     String scopes =
185         OAuth2Utils.validateOptionalString(
186             responseData, OAuth2Utils.TOKEN_RESPONSE_SCOPE, PARSE_ERROR_PREFIX);
187     return AccessToken.newBuilder()
188         .setExpirationTime(new Date(expiresAtMilliseconds))
189         .setTokenValue(accessToken)
190         .setScopes(scopes)
191         .build();
192   }
193 
194   /**
195    * Returns a Google ID Token from the refresh token response.
196    *
197    * @param targetAudience This can't be used for UserCredentials.
198    * @param options list of Credential specific options for the token. Currently unused for
199    *     UserCredentials.
200    * @throws IOException if the attempt to get an IdToken failed
201    * @return IdToken object which includes the raw id_token, expiration and audience
202    */
203   @Override
idTokenWithAudience(String targetAudience, List<Option> options)204   public IdToken idTokenWithAudience(String targetAudience, List<Option> options)
205       throws IOException {
206     GenericData responseData = doRefreshAccessToken();
207     String idTokenKey = "id_token";
208     if (responseData.containsKey(idTokenKey)) {
209       String idTokenString =
210           OAuth2Utils.validateString(responseData, idTokenKey, PARSE_ERROR_PREFIX);
211       return IdToken.create(idTokenString);
212     }
213 
214     throw new IOException(
215         "UserCredentials can obtain an id token only when authenticated through"
216             + " gcloud running 'gcloud auth login --update-adc' or 'gcloud auth application-default"
217             + " login'. The latter form would not work for Cloud Run, but would still generate an"
218             + " id token.");
219   }
220 
221   /**
222    * Returns client ID of the credential from the console.
223    *
224    * @return client ID
225    */
getClientId()226   public final String getClientId() {
227     return clientId;
228   }
229 
230   /**
231    * Returns client secret of the credential from the console.
232    *
233    * @return client secret
234    */
getClientSecret()235   public final String getClientSecret() {
236     return clientSecret;
237   }
238 
239   /**
240    * Returns the refresh token resulting from a OAuth2 consent flow.
241    *
242    * @return refresh token
243    */
getRefreshToken()244   public final String getRefreshToken() {
245     return refreshToken;
246   }
247 
248   /**
249    * Does refresh access token request
250    *
251    * @return Refresh token response data
252    */
doRefreshAccessToken()253   private GenericData doRefreshAccessToken() throws IOException {
254     if (refreshToken == null) {
255       throw new IllegalStateException(
256           "UserCredentials instance cannot refresh because there is no refresh token.");
257     }
258     GenericData tokenRequest = new GenericData();
259     tokenRequest.set("client_id", clientId);
260     tokenRequest.set("client_secret", clientSecret);
261     tokenRequest.set("refresh_token", refreshToken);
262     tokenRequest.set("grant_type", GRANT_TYPE);
263     UrlEncodedContent content = new UrlEncodedContent(tokenRequest);
264 
265     HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
266     HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content);
267     request.setParser(new JsonObjectParser(JSON_FACTORY));
268     HttpResponse response;
269 
270     try {
271       response = request.execute();
272     } catch (HttpResponseException re) {
273       throw GoogleAuthException.createWithTokenEndpointResponseException(re);
274     } catch (IOException e) {
275       throw GoogleAuthException.createWithTokenEndpointIOException(e);
276     }
277 
278     return response.parseAs(GenericData.class);
279   }
280 
281   /**
282    * Returns the instance of InputStream containing the following user credentials in JSON format: -
283    * RefreshToken - ClientId - ClientSecret - ServerTokenUri
284    *
285    * @return user credentials stream
286    */
getUserCredentialsStream()287   private InputStream getUserCredentialsStream() throws IOException {
288     GenericJson json = new GenericJson();
289     json.put("type", GoogleCredentials.USER_FILE_TYPE);
290     if (refreshToken != null) {
291       json.put("refresh_token", refreshToken);
292     }
293     if (tokenServerUri != null) {
294       json.put("token_server_uri", tokenServerUri);
295     }
296     if (clientId != null) {
297       json.put("client_id", clientId);
298     }
299     if (clientSecret != null) {
300       json.put("client_secret", clientSecret);
301     }
302     if (quotaProjectId != null) {
303       json.put("quota_project", clientSecret);
304     }
305     json.setFactory(JSON_FACTORY);
306     String text = json.toPrettyString();
307     return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
308   }
309 
310   /**
311    * Saves the end user credentials into the given file path.
312    *
313    * @param filePath Path to file where to store the credentials
314    * @throws IOException An error storing the credentials.
315    */
save(String filePath)316   public void save(String filePath) throws IOException {
317     OAuth2Utils.writeInputStreamToFile(getUserCredentialsStream(), filePath);
318   }
319 
320   @Override
hashCode()321   public int hashCode() {
322     // We include access token explicitly here for backwards compatibility.
323     // For the rest of the credentials we don't include it because Credentials are
324     // equivalent with different valid active tokens if main and parent fields are equal.
325     return Objects.hash(
326         super.hashCode(),
327         getAccessToken(),
328         clientId,
329         clientSecret,
330         refreshToken,
331         tokenServerUri,
332         transportFactoryClassName,
333         quotaProjectId);
334   }
335 
336   @Override
toString()337   public String toString() {
338     return MoreObjects.toStringHelper(this)
339         .add("requestMetadata", getRequestMetadataInternal())
340         .add("temporaryAccess", getAccessToken())
341         .add("clientId", clientId)
342         .add("refreshToken", refreshToken)
343         .add("tokenServerUri", tokenServerUri)
344         .add("transportFactoryClassName", transportFactoryClassName)
345         .add("quotaProjectId", quotaProjectId)
346         .toString();
347   }
348 
349   @Override
equals(Object obj)350   public boolean equals(Object obj) {
351     if (!(obj instanceof UserCredentials)) {
352       return false;
353     }
354 
355     UserCredentials other = (UserCredentials) obj;
356     return super.equals(other)
357         && Objects.equals(this.getAccessToken(), other.getAccessToken())
358         && Objects.equals(this.clientId, other.clientId)
359         && Objects.equals(this.clientSecret, other.clientSecret)
360         && Objects.equals(this.refreshToken, other.refreshToken)
361         && Objects.equals(this.tokenServerUri, other.tokenServerUri)
362         && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
363         && Objects.equals(this.quotaProjectId, other.quotaProjectId);
364   }
365 
readObject(ObjectInputStream input)366   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
367     input.defaultReadObject();
368     transportFactory = newInstance(transportFactoryClassName);
369   }
370 
newBuilder()371   public static Builder newBuilder() {
372     return new Builder();
373   }
374 
375   @Override
toBuilder()376   public Builder toBuilder() {
377     return new Builder(this);
378   }
379 
380   public static class Builder extends GoogleCredentials.Builder {
381 
382     private String clientId;
383     private String clientSecret;
384     private String refreshToken;
385     private URI tokenServerUri;
386     private HttpTransportFactory transportFactory;
387 
Builder()388     protected Builder() {}
389 
Builder(UserCredentials credentials)390     protected Builder(UserCredentials credentials) {
391       super(credentials);
392       this.clientId = credentials.clientId;
393       this.clientSecret = credentials.clientSecret;
394       this.refreshToken = credentials.refreshToken;
395       this.transportFactory = credentials.transportFactory;
396       this.tokenServerUri = credentials.tokenServerUri;
397     }
398 
399     @CanIgnoreReturnValue
setClientId(String clientId)400     public Builder setClientId(String clientId) {
401       this.clientId = clientId;
402       return this;
403     }
404 
405     @CanIgnoreReturnValue
setClientSecret(String clientSecret)406     public Builder setClientSecret(String clientSecret) {
407       this.clientSecret = clientSecret;
408       return this;
409     }
410 
411     @CanIgnoreReturnValue
setRefreshToken(String refreshToken)412     public Builder setRefreshToken(String refreshToken) {
413       this.refreshToken = refreshToken;
414       return this;
415     }
416 
417     @CanIgnoreReturnValue
setTokenServerUri(URI tokenServerUri)418     public Builder setTokenServerUri(URI tokenServerUri) {
419       this.tokenServerUri = tokenServerUri;
420       return this;
421     }
422 
423     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)424     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
425       this.transportFactory = transportFactory;
426       return this;
427     }
428 
429     @Override
430     @CanIgnoreReturnValue
setAccessToken(AccessToken token)431     public Builder setAccessToken(AccessToken token) {
432       super.setAccessToken(token);
433       return this;
434     }
435 
436     @Override
437     @CanIgnoreReturnValue
setExpirationMargin(Duration expirationMargin)438     public Builder setExpirationMargin(Duration expirationMargin) {
439       super.setExpirationMargin(expirationMargin);
440       return this;
441     }
442 
443     @Override
444     @CanIgnoreReturnValue
setRefreshMargin(Duration refreshMargin)445     public Builder setRefreshMargin(Duration refreshMargin) {
446       super.setRefreshMargin(refreshMargin);
447       return this;
448     }
449 
450     @Override
451     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)452     public Builder setQuotaProjectId(String quotaProjectId) {
453       super.setQuotaProjectId(quotaProjectId);
454       return this;
455     }
456 
getClientId()457     public String getClientId() {
458       return clientId;
459     }
460 
getClientSecret()461     public String getClientSecret() {
462       return clientSecret;
463     }
464 
getRefreshToken()465     public String getRefreshToken() {
466       return refreshToken;
467     }
468 
getTokenServerUri()469     public URI getTokenServerUri() {
470       return tokenServerUri;
471     }
472 
getHttpTransportFactory()473     public HttpTransportFactory getHttpTransportFactory() {
474       return transportFactory;
475     }
476 
477     @Override
build()478     public UserCredentials build() {
479       return new UserCredentials(this);
480     }
481   }
482 }
483