diff --git a/.gitignore b/.gitignore index 75c0e0cc1..bdf3ed927 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target/ # Intellij *.iml +*.factorypath .idea/ # VS Code diff --git a/credentials/java/com/google/auth/Credentials.java b/credentials/java/com/google/auth/Credentials.java index c52303a48..416de9d5a 100644 --- a/credentials/java/com/google/auth/Credentials.java +++ b/credentials/java/com/google/auth/Credentials.java @@ -129,7 +129,9 @@ protected final void blockingGetToCallback(URI uri, RequestMetadataCallback call * * @param uri URI of the entry point for the request. * @return The request metadata used for populating headers or other context. - * @throws IOException if there was an error getting up-to-date access. + * @throws IOException if there was an error getting up-to-date access. The exception should + * implement {@link Retryable} and {@code isRetryable()} will return true if the operation may + * be retried. */ public abstract Map> getRequestMetadata(URI uri) throws IOException; diff --git a/credentials/java/com/google/auth/Retryable.java b/credentials/java/com/google/auth/Retryable.java new file mode 100644 index 000000000..a25410ebf --- /dev/null +++ b/credentials/java/com/google/auth/Retryable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Google LLC + * + * 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 LLC 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; + +// an interface to identify retryable errors +public interface Retryable { + /** + * A flag indicating whether the error is retryable + * + * @return true if related error is retryable, false otherwise + */ + boolean isRetryable(); + + /** + * Gets a number of performed retries for related HttpRequest + * + * @return a number of performed retries + */ + int getRetryCount(); +} diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleAuthException.java b/oauth2_http/java/com/google/auth/oauth2/GoogleAuthException.java new file mode 100644 index 000000000..4ab42b7e6 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/GoogleAuthException.java @@ -0,0 +1,157 @@ +/* + * Copyright 2022 Google LLC + * + * 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 LLC 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 com.google.api.client.http.HttpResponseException; +import com.google.auth.Retryable; +import java.io.IOException; + +/** + * Base class for the standard Auth error response. It extends a default exception while keeping + * Json response format + */ +class GoogleAuthException extends IOException implements Retryable { + + private final boolean isRetryable; + private final int retryCount; + + /** + * Constructor with all parameters + * + * @param isRetryable A retry status for the related HTTP request + * @param retryCount A number of retries performed for the related HTTP request + * @param message The detail message (which is saved for later retrieval by the {@link + * #getMessage()} method) + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + GoogleAuthException(boolean isRetryable, int retryCount, String message, Throwable cause) { + super(message, cause); + this.isRetryable = isRetryable; + this.retryCount = retryCount; + } + + /** + * Constructor with message defaulted to the cause + * + * @param isRetryable A retry status for the related HTTP request + * @param retryCount A number of retries performed for the related HTTP request + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) If the + * cause has retry information, it is going to be skipped in favor of the {@code retryCount} + * parameter + */ + GoogleAuthException(boolean isRetryable, int retryCount, Throwable cause) { + super(cause); + this.isRetryable = isRetryable; + this.retryCount = retryCount; + } + + /** + * Constructor without explicit retry count. + * + * @param isRetryable A retry status for the related HTTP request + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + GoogleAuthException(boolean isRetryable, Throwable cause) { + super(cause); + this.isRetryable = isRetryable; + this.retryCount = 0; + } + + /** + * Constructor without retry info + * + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + GoogleAuthException(Throwable cause) { + this(false, cause); + } + + /** A default Constructor */ + GoogleAuthException() { + super(); + this.isRetryable = false; + this.retryCount = 0; + } + + /** + * Creates an instance of the exception based on {@link HttpResponseException} and a custom error + * message. + * + * @see #createWithTokenEndpointResponseException(HttpResponseException, String) + * @param responseException an instance of {@link HttpResponseException} + * @param message The detail message (which is saved for later retrieval by the {@link + * #getMessage()} method) + * @return new instance of {@link GoogleAuthException} + */ + static GoogleAuthException createWithTokenEndpointResponseException( + HttpResponseException responseException, String message) { + int responseStatus = responseException.getStatusCode(); + boolean isRetryable = + OAuth2Utils.TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES.contains(responseStatus); + int retryCount = responseException.getAttemptCount() - 1; + + if (message == null) { + return new GoogleAuthException(isRetryable, retryCount, responseException); + } else { + return new GoogleAuthException(isRetryable, retryCount, message, responseException); + } + } + + /** + * Creates an instance of the exception based on {@link HttpResponseException} returned by Google + * token endpoint. It uses response status code information to populate the {@code #isRetryable} + * property and a number of performed attempts to populate the {@code #retryCount} property + * + * @param responseException an instance of {@link HttpResponseException} + * @return new instance of {@link GoogleAuthException} + */ + static GoogleAuthException createWithTokenEndpointResponseException( + HttpResponseException responseException) { + return GoogleAuthException.createWithTokenEndpointResponseException(responseException, null); + } + + /** Returns true if the error is retryable, false otherwise */ + @Override + public boolean isRetryable() { + return isRetryable; + } + + /** Returns number of reties performed for the related HTTP request */ + @Override + public int getRetryCount() { + return retryCount; + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java index 235b4eeb1..613c5c2da 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @@ -243,8 +243,8 @@ public boolean createScopedRequired() { } /** - * If the credentials support scopes, creates a copy of the the identity with the specified - * scopes; otherwise, returns the same instance. + * If the credentials support scopes, creates a copy of the identity with the specified scopes; + * otherwise, returns the same instance. * * @param scopes Collection of scopes to request. * @return GoogleCredentials with requested scopes. @@ -254,9 +254,8 @@ public GoogleCredentials createScoped(Collection scopes) { } /** - * If the credentials support scopes, creates a copy of the the identity with the specified scopes - * and default scopes; otherwise, returns the same instance. This is mainly used by client - * libraries. + * If the credentials support scopes, creates a copy of the identity with the specified scopes and + * default scopes; otherwise, returns the same instance. This is mainly used by client libraries. * * @param scopes Collection of scopes to request. * @param defaultScopes Collection of default scopes to request. @@ -268,8 +267,8 @@ public GoogleCredentials createScoped( } /** - * If the credentials support scopes, creates a copy of the the identity with the specified - * scopes; otherwise, returns the same instance. + * If the credentials support scopes, creates a copy of the identity with the specified scopes; + * otherwise, returns the same instance. * * @param scopes Collection of scopes to request. * @return GoogleCredentials with requested scopes. @@ -278,6 +277,17 @@ public GoogleCredentials createScoped(String... scopes) { return createScoped(ImmutableList.copyOf(scopes)); } + /** + * If the credentials support automatic retries, creates a copy of the identity with the provided + * retry strategy + * + * @param defaultRetriesEnabled a flag enabling or disabling default retries + * @return GoogleCredentials with the new default retries configuration. + */ + public GoogleCredentials createWithCustomRetryStrategy(boolean defaultRetriesEnabled) { + return this; + } + /** * If the credentials support domain-wide delegation, creates a copy of the identity so that it * impersonates the specified user; otherwise, returns the same instance. diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index 9ba5ce8d7..eaa33a11e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java @@ -50,8 +50,11 @@ import java.math.BigDecimal; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** Internal utilities for the com.google.auth.oauth2 namespace. */ class OAuth2Utils { @@ -74,6 +77,11 @@ class OAuth2Utils { static final String BEARER_PREFIX = AuthHttpConstants.BEARER + " "; + // Includes expected server errors from Google token endpoint + // Other 5xx codes are either not used or retries are unlikely to succeed + public static final Set TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES = + new HashSet<>(Arrays.asList(500, 503, 408, 429)); + static class DefaultHttpTransportFactory implements HttpTransportFactory { public HttpTransport create() { diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java index b3f612a04..a920b93d0 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java @@ -33,14 +33,13 @@ import static com.google.common.base.Preconditions.checkNotNull; -import java.io.IOException; import javax.annotation.Nullable; /** * Encapsulates the standard OAuth error response. See * https://tools.ietf.org/html/rfc6749#section-5.2. */ -class OAuthException extends IOException { +class OAuthException extends GoogleAuthException { private final String errorCode; @Nullable private final String errorDescription; diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index 1588c1613..02aff547f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -34,12 +34,12 @@ import static com.google.common.base.MoreObjects.firstNonNull; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpBackOffIOExceptionHandler; import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler.BackOffRequired; 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; @@ -97,6 +97,10 @@ public class ServiceAccountCredentials extends GoogleCredentials private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; private static final int TWELVE_HOURS_IN_SECONDS = 43200; private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600; + private static final int DEFAULT_NUMBER_OF_RETRIES = 3; + private static final int INITIAL_RETRY_INTERVAL_MILLIS = 1000; + private static final double RETRY_RANDOMIZATION_FACTOR = 0.1; + private static final double RETRY_MULTIPLIER = 2; private final String clientId; private final String clientEmail; @@ -111,66 +115,43 @@ public class ServiceAccountCredentials extends GoogleCredentials private final String quotaProjectId; private final int lifetime; private final boolean useJwtAccessWithScope; + private final boolean defaultRetriesEnabled; private transient HttpTransportFactory transportFactory; /** - * Constructor with minimum identifying information and custom HTTP transport. + * Internal constructor * - * @param clientId client ID of the service account from the console. May be null. - * @param clientEmail client email address of the service account from the console - * @param privateKey RSA private key object for the service account - * @param privateKeyId private key identifier for the service account. May be null. - * @param scopes scope strings for the APIs to be called. May be null or an empty collection. - * @param defaultScopes default scope strings for the APIs to be called. May be null or an empty. - * @param transportFactory HTTP transport factory, creates the transport used to get access - * tokens. - * @param tokenServerUri URI of the end point that provides tokens. - * @param serviceAccountUser email of the user account to impersonate, if delegating domain-wide - * authority to the service account. - * @param projectId the project used for billing - * @param quotaProjectId the project used for quota and billing purposes. May be null. - * @param lifetime number of seconds the access token should be valid for. The value should be at - * most 43200 (12 hours). If the token is used for calling a Google API, then the value should - * be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used - * when creating the credentials. - * @param useJwtAccessWithScope whether self signed JWT with scopes should be always used. + * @param builder A builder for {@link ServiceAccountCredentials} See {@link + * ServiceAccountCredentials.Builder} */ - ServiceAccountCredentials( - String clientId, - String clientEmail, - PrivateKey privateKey, - String privateKeyId, - Collection scopes, - Collection defaultScopes, - HttpTransportFactory transportFactory, - URI tokenServerUri, - String serviceAccountUser, - String projectId, - String quotaProjectId, - int lifetime, - boolean useJwtAccessWithScope) { - this.clientId = clientId; - this.clientEmail = Preconditions.checkNotNull(clientEmail); - this.privateKey = Preconditions.checkNotNull(privateKey); - this.privateKeyId = privateKeyId; - this.scopes = (scopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(scopes); + ServiceAccountCredentials(ServiceAccountCredentials.Builder builder) { + this.clientId = builder.clientId; + this.clientEmail = Preconditions.checkNotNull(builder.clientEmail); + this.privateKey = Preconditions.checkNotNull(builder.privateKey); + this.privateKeyId = builder.privateKeyId; + this.scopes = + (builder.scopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(builder.scopes); this.defaultScopes = - (defaultScopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(defaultScopes); + (builder.defaultScopes == null) + ? ImmutableSet.of() + : ImmutableSet.copyOf(builder.defaultScopes); this.transportFactory = firstNonNull( - transportFactory, + builder.transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.transportFactoryClassName = this.transportFactory.getClass().getName(); - this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri; - this.serviceAccountUser = serviceAccountUser; - this.projectId = projectId; - this.quotaProjectId = quotaProjectId; - if (lifetime > TWELVE_HOURS_IN_SECONDS) { + this.tokenServerUri = + (builder.tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : builder.tokenServerUri; + this.serviceAccountUser = builder.serviceAccountUser; + this.projectId = builder.projectId; + this.quotaProjectId = builder.quotaProjectId; + if (builder.lifetime > TWELVE_HOURS_IN_SECONDS) { throw new IllegalStateException("lifetime must be less than or equal to 43200"); } - this.lifetime = lifetime; - this.useJwtAccessWithScope = useJwtAccessWithScope; + this.lifetime = builder.lifetime; + this.useJwtAccessWithScope = builder.useJwtAccessWithScope; + this.defaultRetriesEnabled = builder.defaultRetriesEnabled; } /** @@ -209,18 +190,17 @@ static ServiceAccountCredentials fromJson( + "expecting 'client_id', 'client_email', 'private_key' and 'private_key_id'."); } - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - null, - null, - transportFactory, - tokenServerUriFromCreds, - null, - projectId, - quotaProjectId); + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setHttpTransportFactory(transportFactory) + .setTokenServerUri(tokenServerUriFromCreds) + .setProjectId(projectId) + .setQuotaProjectId(quotaProjectId); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -242,18 +222,14 @@ public static ServiceAccountCredentials fromPkcs8( String privateKeyId, Collection scopes) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - null, - null, - null, - null, - null, - null); + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -276,18 +252,14 @@ public static ServiceAccountCredentials fromPkcs8( Collection scopes, Collection defaultScopes) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - defaultScopes, - null, - null, - null, - null, - null); + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes, defaultScopes); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -315,18 +287,17 @@ public static ServiceAccountCredentials fromPkcs8( HttpTransportFactory transportFactory, URI tokenServerUri) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - null, - transportFactory, - tokenServerUri, - null, - null, - null); + + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes) + .setHttpTransportFactory(transportFactory) + .setTokenServerUri(tokenServerUri); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -357,18 +328,17 @@ public static ServiceAccountCredentials fromPkcs8( HttpTransportFactory transportFactory, URI tokenServerUri) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - defaultScopes, - transportFactory, - tokenServerUri, - null, - null, - null); + + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes, defaultScopes) + .setHttpTransportFactory(transportFactory) + .setTokenServerUri(tokenServerUri); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -399,18 +369,18 @@ public static ServiceAccountCredentials fromPkcs8( URI tokenServerUri, String serviceAccountUser) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - null, - transportFactory, - tokenServerUri, - serviceAccountUser, - null, - null); + + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes) + .setHttpTransportFactory(transportFactory) + .setTokenServerUri(tokenServerUri) + .setServiceAccountUser(serviceAccountUser); + + return fromPkcs8(privateKeyPkcs8, builder); } /** @@ -444,48 +414,33 @@ public static ServiceAccountCredentials fromPkcs8( URI tokenServerUri, String serviceAccountUser) throws IOException { - return fromPkcs8( - clientId, - clientEmail, - privateKeyPkcs8, - privateKeyId, - scopes, - defaultScopes, - transportFactory, - tokenServerUri, - serviceAccountUser, - null, - null); + ServiceAccountCredentials.Builder builder = + ServiceAccountCredentials.newBuilder() + .setClientId(clientId) + .setClientEmail(clientEmail) + .setPrivateKeyId(privateKeyId) + .setScopes(scopes, defaultScopes) + .setHttpTransportFactory(transportFactory) + .setTokenServerUri(tokenServerUri) + .setServiceAccountUser(serviceAccountUser); + + return fromPkcs8(privateKeyPkcs8, builder); } + /** + * Internal constructor + * + * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. + * @param builder A builder for {@link ServiceAccountCredentials} See {@link + * ServiceAccountCredentials.Builder} + * @return an instance of {@link ServiceAccountCredentials} + */ static ServiceAccountCredentials fromPkcs8( - String clientId, - String clientEmail, - String privateKeyPkcs8, - String privateKeyId, - Collection scopes, - Collection defaultScopes, - HttpTransportFactory transportFactory, - URI tokenServerUri, - String serviceAccountUser, - String projectId, - String quotaProject) - throws IOException { + String privateKeyPkcs8, ServiceAccountCredentials.Builder builder) throws IOException { PrivateKey privateKey = privateKeyFromPkcs8(privateKeyPkcs8); - return new ServiceAccountCredentials( - clientId, - clientEmail, - privateKey, - privateKeyId, - scopes, - defaultScopes, - transportFactory, - tokenServerUri, - serviceAccountUser, - projectId, - quotaProject, - DEFAULT_LIFETIME_IN_SECONDS, - false); + builder.setPrivateKey(privateKey); + + return new ServiceAccountCredentials(builder); } /** Helper to convert from a PKCS#8 String to an RSA private key */ @@ -576,36 +531,42 @@ public AccessToken refreshAccessToken() throws IOException { HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content); + + if (this.defaultRetriesEnabled) { + request.setNumberOfRetries(DEFAULT_NUMBER_OF_RETRIES); + } else { + request.setNumberOfRetries(0); + } request.setParser(new JsonObjectParser(jsonFactory)); - request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff())); + ExponentialBackOff backoff = + new ExponentialBackOff.Builder() + .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL_MILLIS) + .setRandomizationFactor(RETRY_RANDOMIZATION_FACTOR) + .setMultiplier(RETRY_MULTIPLIER) + .build(); + request.setUnsuccessfulResponseHandler( - new HttpBackOffUnsuccessfulResponseHandler(new ExponentialBackOff()) + new HttpBackOffUnsuccessfulResponseHandler(backoff) .setBackOffRequired( new BackOffRequired() { public boolean isRequired(HttpResponse response) { int code = response.getStatusCode(); - return ( - // Server error --- includes timeout errors, which use 500 instead of 408 - code / 100 == 5 - // Forbidden error --- for historical reasons, used for rate_limit_exceeded - // errors instead of 429, but there currently seems no robust automatic way - // to - // distinguish these cases: see - // https://github.com/google/google-api-java-client/issues/662 - || code == 403); + + return OAuth2Utils.TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES.contains(code); } })); HttpResponse response; + String errorTemplate = "Error getting access token for service account: %s, iss: %s"; + try { response = request.execute(); + } catch (HttpResponseException re) { + String message = String.format(errorTemplate, re.getMessage(), getIssuer()); + throw GoogleAuthException.createWithTokenEndpointResponseException(re, message); } catch (IOException e) { - throw new IOException( - String.format( - "Error getting access token for service account: %s, iss: %s", - e.getMessage(), getIssuer()), - e); + throw new IOException(String.format(errorTemplate, e.getMessage(), getIssuer()), e); } GenericData responseData = response.parseAs(GenericData.class); @@ -621,7 +582,7 @@ public boolean isRequired(HttpResponse response) { * Returns a Google ID Token from the metadata server on ComputeEngine. * * @param targetAudience the aud: field the IdToken should include. - * @param options list of Credential specific options for for the token. Currently unused for + * @param options list of Credential specific options for the token. Currently, unused for * ServiceAccountCredentials. * @throws IOException if the attempt to get an IdToken failed * @return IdToken object which includes the raw id_token, expiration and audience @@ -661,6 +622,17 @@ public IdToken idTokenWithAudience(String targetAudience, List