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