/* * Copyright 2015, Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.google.auth.oauth2; import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY; import static com.google.common.base.MoreObjects.firstNonNull; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.GenericData; import com.google.api.client.util.Preconditions; import com.google.auth.http.HttpTransportFactory; import com.google.common.base.MoreObjects; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; /** OAuth2 Credentials representing a user's identity and consent. */ public class UserCredentials extends GoogleCredentials implements IdTokenProvider { private static final String GRANT_TYPE = "refresh_token"; private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; private static final long serialVersionUID = -4800758775038679176L; private final String clientId; private final String clientSecret; private final String refreshToken; private final URI tokenServerUri; private final String transportFactoryClassName; private transient HttpTransportFactory transportFactory; /** * Internal constructor * * @param builder A builder for {@link UserCredentials} See {@link UserCredentials.Builder} */ private UserCredentials(Builder builder) { super(builder); this.clientId = Preconditions.checkNotNull(builder.clientId); this.clientSecret = Preconditions.checkNotNull(builder.clientSecret); this.refreshToken = builder.refreshToken; this.transportFactory = firstNonNull( builder.transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.tokenServerUri = (builder.tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : builder.tokenServerUri; this.transportFactoryClassName = this.transportFactory.getClass().getName(); Preconditions.checkState( builder.getAccessToken() != null || builder.refreshToken != null, "Either accessToken or refreshToken must not be null"); } /** * Returns user credentials defined by JSON contents using the format supported by the Cloud SDK. * * @param json a map from the JSON representing the credentials. * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @return the credentials defined by the JSON. * @throws IOException if the credential cannot be created from the JSON. */ static UserCredentials fromJson(Map json, HttpTransportFactory transportFactory) throws IOException { String clientId = (String) json.get("client_id"); String clientSecret = (String) json.get("client_secret"); String refreshToken = (String) json.get("refresh_token"); String quotaProjectId = (String) json.get("quota_project_id"); if (clientId == null || clientSecret == null || refreshToken == null) { throw new IOException( "Error reading user credential from JSON, " + " expecting 'client_id', 'client_secret' and 'refresh_token'."); } return UserCredentials.newBuilder() .setClientId(clientId) .setClientSecret(clientSecret) .setRefreshToken(refreshToken) .setAccessToken(null) .setHttpTransportFactory(transportFactory) .setTokenServerUri(null) .setQuotaProjectId(quotaProjectId) .build(); } /** * Returns credentials defined by a JSON file stream using the format supported by the Cloud SDK. * * @param credentialsStream the stream with the credential definition. * @return the credential defined by the credentialsStream. * @throws IOException if the credential cannot be created from the stream. */ public static UserCredentials fromStream(InputStream credentialsStream) throws IOException { return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY); } /** * Returns credentials defined by a JSON file stream using the format supported by the Cloud SDK. * * @param credentialsStream the stream with the credential definition. * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @return the credential defined by the credentialsStream. * @throws IOException if the credential cannot be created from the stream. */ public static UserCredentials fromStream( InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException { Preconditions.checkNotNull(credentialsStream); Preconditions.checkNotNull(transportFactory); JsonFactory jsonFactory = JSON_FACTORY; JsonObjectParser parser = new JsonObjectParser(jsonFactory); GenericJson fileContents = parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class); String fileType = (String) fileContents.get("type"); if (fileType == null) { throw new IOException("Error reading credentials from stream, 'type' field not specified."); } if (USER_FILE_TYPE.equals(fileType)) { return fromJson(fileContents, transportFactory); } throw new IOException( String.format( "Error reading credentials from stream, 'type' value '%s' not recognized." + " Expecting '%s'.", fileType, USER_FILE_TYPE)); } /** Refreshes the OAuth2 access token by getting a new access token from the refresh token */ @Override public AccessToken refreshAccessToken() throws IOException { GenericData responseData = doRefreshAccessToken(); String accessToken = OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); int expiresInSeconds = OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000; String scopes = OAuth2Utils.validateOptionalString( responseData, OAuth2Utils.TOKEN_RESPONSE_SCOPE, PARSE_ERROR_PREFIX); return AccessToken.newBuilder() .setExpirationTime(new Date(expiresAtMilliseconds)) .setTokenValue(accessToken) .setScopes(scopes) .build(); } /** * Returns a Google ID Token from the refresh token response. * * @param targetAudience This can't be used for UserCredentials. * @param options list of Credential specific options for the token. Currently unused for * UserCredentials. * @throws IOException if the attempt to get an IdToken failed * @return IdToken object which includes the raw id_token, expiration and audience */ @Override public IdToken idTokenWithAudience(String targetAudience, List